Compare commits
10
Commits
a842c884e8
...
b36d438132
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b36d438132 | ||
|
|
3e87a67809 | ||
|
|
0b6d2be43f | ||
|
|
301716e869 | ||
|
|
be402cc9be | ||
|
|
ca34e077ad | ||
|
|
51601545ae | ||
|
|
c4b59acc68 | ||
|
|
e3100b4718 | ||
|
|
1d1a14318c |
+15
-9
@@ -1,9 +1,15 @@
|
||||
Tests/build/
|
||||
Programs/build/
|
||||
Object/
|
||||
Assembler
|
||||
SplitBit
|
||||
SplitDisk
|
||||
CLAUDE.md
|
||||
claudeResume.sh
|
||||
codexResume.sh
|
||||
# Anchored to the repository root, all of them. Without the leading slash a pattern
|
||||
# matches at any depth, so "Assembler" also ignored the Source/Assembler DIRECTORY - and a
|
||||
# new source file added there would have been silently untracked.
|
||||
/Tests/build/
|
||||
/Programs/build/
|
||||
/Object/
|
||||
/Assembler
|
||||
/SplitBit
|
||||
/SplitDisk
|
||||
/CLAUDE.md
|
||||
/claudeResume.sh
|
||||
/codexResume.sh
|
||||
|
||||
# Python leaves these beside the test scripts.
|
||||
__pycache__/
|
||||
|
||||
@@ -0,0 +1,846 @@
|
||||
; Edit, a line editor for CosmOS.
|
||||
;
|
||||
; The first program on this machine that makes a file a person typed. Everything on every
|
||||
; disk before this one was put there by the host tool.
|
||||
;
|
||||
; It is line oriented, in the manner of ed, and that is a deliberate choice rather than a
|
||||
; limitation of the machine - Snake already draws a whole screen and steers with single
|
||||
; keys. A full screen editor wants scrolling, a redraw model and cursor arithmetic, none of
|
||||
; which teaches anything about files, and files are what this exists to exercise. So it
|
||||
; stays in line mode and reads whole lines, which is what the console does without being
|
||||
; asked for anything.
|
||||
;
|
||||
; l list the whole thing, numbered
|
||||
; a add lines at the end, until a line that is just a dot
|
||||
; i <n> put lines in before line n, the same way
|
||||
; c <n> change line n
|
||||
; d <n> delete line n
|
||||
; w write it back
|
||||
; q stop without writing
|
||||
;
|
||||
; ---- How the text is kept ----
|
||||
;
|
||||
; A LINKED LIST OF LINES, not one buffer with newlines in it. Each line is a node holding
|
||||
; where the next one is, how long it is, and its bytes:
|
||||
;
|
||||
; 0 2 where the next line is, or zero
|
||||
; 2 1 how many bytes this line has
|
||||
; 3 the bytes
|
||||
;
|
||||
; Inserting is then two pointers changed and nothing moved, and so is deleting. With one
|
||||
; flat buffer both of them would mean shifting everything after the edit, which on a
|
||||
; machine with no memcpy is a loop over every byte of the rest of the document, run for
|
||||
; every keystroke's worth of editing.
|
||||
;
|
||||
; The price is that DELETED LINES ARE NOT REUSED. A new line always goes at the end of the
|
||||
; arena, and an unlinked one just sits there. A session that edits heavily uses more room
|
||||
; than the document needs, and writing the file out and reading it back is what tidies it
|
||||
; up. That is an honest trade for a program this size, and it is written down here rather
|
||||
; than left as a surprise.
|
||||
;
|
||||
; Two regions are used by arrangement rather than reserved, because reserving them would
|
||||
; put tens of kilobytes of zeroes into the file for no reason:
|
||||
;
|
||||
; 0x4000 the file, on its way in or out
|
||||
; 0x8000 the arena the lines live in
|
||||
;
|
||||
; Nothing is running but this, so both are ours. It is the same arrangement CosmOS makes
|
||||
; with 0x8000 while it is loading something, for the same reason.
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x2000
|
||||
|
||||
start:
|
||||
SETD.0 FileName
|
||||
INIB 0d23
|
||||
SWI osArgument
|
||||
SETD.0 FileName
|
||||
LDA.0
|
||||
BRA noName
|
||||
|
||||
; Everything is set here rather than trusted to be zero, since running a program a second
|
||||
; time does not load it again.
|
||||
RSTA
|
||||
SETD.0 TextHead
|
||||
STA.0
|
||||
INCD.0
|
||||
STA.0
|
||||
SETD.0 ArenaFree
|
||||
INIA 0x80
|
||||
STA.0
|
||||
INCD.0
|
||||
RSTA
|
||||
STA.0
|
||||
|
||||
CALL loadFile
|
||||
|
||||
SETD.0 FileName
|
||||
SWI osPrintString
|
||||
SETD.0 CommaText
|
||||
SWI osPrintString
|
||||
CALL countLines
|
||||
MVQA
|
||||
CALL printByte
|
||||
; One line is not one lines. The same care the shell's file listing takes, for the same
|
||||
; reason: it costs four instructions and reads as carelessness without them.
|
||||
DECA
|
||||
BRA oneLine
|
||||
SETD.0 LinesText
|
||||
BRI sayLines
|
||||
oneLine:
|
||||
SETD.0 LineText
|
||||
sayLines:
|
||||
SWI osPrintString
|
||||
CALL newLine
|
||||
|
||||
commandLoop:
|
||||
SETD.0 PromptText
|
||||
SWI osPrintString
|
||||
SETD.0 Command
|
||||
INIB 0d40
|
||||
SWI osReadLine
|
||||
|
||||
; Running out of typing ends it, the same way it ends the shell.
|
||||
INA 0x01
|
||||
INIB 0x02 ; ENDED
|
||||
AND
|
||||
BNQ quit
|
||||
|
||||
SETD.0 Command
|
||||
LDA.0
|
||||
BRA commandLoop ; An empty line asks for nothing.
|
||||
|
||||
; Whatever number follows the letter, if there is one. The spaces between the two are
|
||||
; stepped over first: a number is what somebody typed after "d ", not after "d".
|
||||
SETD.0 Command
|
||||
INCD.0
|
||||
commandSpaces:
|
||||
LDA.0
|
||||
INIB 0x20
|
||||
XOR
|
||||
BNQ commandArgument
|
||||
INCD.0
|
||||
BRI commandSpaces
|
||||
commandArgument:
|
||||
CALL textNumber
|
||||
MVQA
|
||||
SETD.0 Wanted
|
||||
STA.0
|
||||
|
||||
SETD.0 Command
|
||||
LDA.0
|
||||
|
||||
INIB 0d108 ; l
|
||||
XOR
|
||||
BRQ doList
|
||||
INIB 0d97 ; a
|
||||
XOR
|
||||
BRQ doAppend
|
||||
INIB 0d105 ; i
|
||||
XOR
|
||||
BRQ doInsert
|
||||
INIB 0d99 ; c
|
||||
XOR
|
||||
BRQ doChange
|
||||
INIB 0d100 ; d
|
||||
XOR
|
||||
BRQ doDelete
|
||||
INIB 0d119 ; w
|
||||
XOR
|
||||
BRQ doWrite
|
||||
INIB 0d113 ; q
|
||||
XOR
|
||||
BRQ quit
|
||||
|
||||
SETD.0 WhatText
|
||||
SWI osPrintString
|
||||
CALL newLine
|
||||
BRI commandLoop
|
||||
|
||||
quit:
|
||||
SWI osExit
|
||||
|
||||
noName:
|
||||
SETD.0 NoNameText
|
||||
SWI osPrintString
|
||||
CALL newLine
|
||||
SWI osExit
|
||||
|
||||
|
||||
; ---- The commands ----
|
||||
|
||||
doList:
|
||||
CALL listLines
|
||||
BRI commandLoop
|
||||
|
||||
doAppend:
|
||||
CALL countLines
|
||||
MVQA
|
||||
INCA
|
||||
SETD.0 Wanted
|
||||
STA.0 ; Adding at the end is inserting before the line after it.
|
||||
BRI insertLoop
|
||||
|
||||
doInsert:
|
||||
SETD.0 Wanted
|
||||
LDA.0
|
||||
BRA insertNeedsLine
|
||||
insertLoop:
|
||||
SETD.0 EnteringText
|
||||
SWI osPrintString
|
||||
SETD.0 Entry
|
||||
INIB 0d80
|
||||
SWI osReadLine
|
||||
INA 0x01
|
||||
INIB 0x02 ; ENDED
|
||||
AND
|
||||
BNQ commandLoop
|
||||
|
||||
; A line that is just a dot ends it, which is the oldest convention there is for this.
|
||||
SETD.0 Entry
|
||||
SETD.1 DotText
|
||||
CALL textSame
|
||||
BRQ commandLoop
|
||||
|
||||
SETD.0 Entry
|
||||
CALL makeNode
|
||||
SETD.0 Wanted
|
||||
LDA.0
|
||||
CALL linkBefore
|
||||
SETD.0 Wanted
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0 ; The next one goes after the one just put in.
|
||||
BRI insertLoop
|
||||
|
||||
insertNeedsLine:
|
||||
SETD.0 NeedsLineText
|
||||
SWI osPrintString
|
||||
CALL newLine
|
||||
BRI commandLoop
|
||||
|
||||
doChange:
|
||||
SETD.0 Wanted
|
||||
LDA.0
|
||||
BRA insertNeedsLine
|
||||
CALL findLine
|
||||
BNQ noSuchLine
|
||||
|
||||
SETD.0 EnteringText
|
||||
SWI osPrintString
|
||||
SETD.0 Entry
|
||||
INIB 0d80
|
||||
SWI osReadLine
|
||||
INA 0x01
|
||||
INIB 0x02 ; ENDED
|
||||
AND
|
||||
BNQ commandLoop
|
||||
|
||||
SETD.0 Entry
|
||||
CALL makeNode
|
||||
SETD.0 Wanted
|
||||
LDA.0
|
||||
CALL linkBefore ; The new one goes in front of the old one,
|
||||
SETD.0 Wanted
|
||||
LDA.0
|
||||
INCA
|
||||
CALL unlinkLine ; and the old one, now one further along, comes out.
|
||||
BRI commandLoop
|
||||
|
||||
doDelete:
|
||||
SETD.0 Wanted
|
||||
LDA.0
|
||||
BRA insertNeedsLine
|
||||
CALL unlinkLine
|
||||
BNQ noSuchLine
|
||||
BRI commandLoop
|
||||
|
||||
noSuchLine:
|
||||
SETD.0 NoLineText
|
||||
SWI osPrintString
|
||||
CALL newLine
|
||||
BRI commandLoop
|
||||
|
||||
doWrite:
|
||||
CALL writeFile
|
||||
BNQ writeFailed
|
||||
SETD.0 WrittenText
|
||||
SWI osPrintString
|
||||
SETD.0 WroteSize
|
||||
CALL printWord
|
||||
SETD.0 BytesText
|
||||
SWI osPrintString
|
||||
CALL newLine
|
||||
BRI commandLoop
|
||||
|
||||
writeFailed:
|
||||
SETD.0 NoWriteText
|
||||
SWI osPrintString
|
||||
CALL newLine
|
||||
BRI commandLoop
|
||||
|
||||
; ---- The list of lines ----
|
||||
|
||||
; DP0 is a string. Puts a node holding it at the end of the arena, and leaves DP3 on it.
|
||||
makeNode:
|
||||
SETD.1 ArenaFree
|
||||
LDD.3.1
|
||||
|
||||
PSHD.3
|
||||
POPD.1
|
||||
RSTA
|
||||
STA.1 ; Nothing follows it yet.
|
||||
INCD.1
|
||||
STA.1
|
||||
INCD.1
|
||||
PSHD.1 ; Where the length goes, once it is known.
|
||||
INCD.1
|
||||
RSTB
|
||||
|
||||
makeNodeLoop:
|
||||
LDA.0
|
||||
BRA makeNodeEnd
|
||||
STA.1
|
||||
INCD.0
|
||||
INCD.1
|
||||
INCB
|
||||
BRI makeNodeLoop
|
||||
|
||||
makeNodeEnd:
|
||||
POPD.0
|
||||
PSHB
|
||||
POPA
|
||||
STA.0 ; How long it turned out to be.
|
||||
INIB 0d3
|
||||
CCF
|
||||
ADD
|
||||
MVQA
|
||||
SETD.0 ArenaFree
|
||||
CALL addByteToWord
|
||||
RET
|
||||
|
||||
; A is a line number. Leaves DP3 on that line and PrevLine on the one before it, which is
|
||||
; zero when it is the first. Q is zero if there is such a line.
|
||||
findLine:
|
||||
SETD.1 Wanted2
|
||||
STA.1
|
||||
INIA 0d1
|
||||
SETD.1 Counted
|
||||
STA.1
|
||||
RSTA
|
||||
SETD.1 PrevLine
|
||||
STA.1
|
||||
INCD.1
|
||||
STA.1
|
||||
SETD.1 TextHead
|
||||
LDD.3.1
|
||||
|
||||
findLineStep:
|
||||
PSHD.3
|
||||
POPA
|
||||
POPB
|
||||
OR
|
||||
BRQ findLineMissing
|
||||
|
||||
SETD.1 Counted
|
||||
LDA.1
|
||||
SETD.1 Wanted2
|
||||
LDB.1
|
||||
XOR
|
||||
BRQ findLineFound
|
||||
|
||||
PSHD.3
|
||||
SETD.1 PrevLine
|
||||
POPD.0
|
||||
STD.0.1
|
||||
PSHD.3
|
||||
POPD.0
|
||||
LDD.3.0 ; On to whatever follows it.
|
||||
SETD.1 Counted
|
||||
LDA.1
|
||||
INCA
|
||||
STA.1
|
||||
BRI findLineStep
|
||||
|
||||
findLineFound:
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
findLineMissing:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; DP3 is a new node and A is the line number it should become. Puts it there.
|
||||
linkBefore:
|
||||
PSHD.3
|
||||
SETD.1 NewLine
|
||||
POPD.0
|
||||
STD.0.1 ; The new node, while the old ones are looked through.
|
||||
|
||||
CALL findLine ; Which may miss, and missing means putting it at the end.
|
||||
|
||||
; What the new node should point at is whatever was there, or nothing.
|
||||
SETD.1 NewLine
|
||||
LDD.0.1
|
||||
BNQ linkBeforeAtEnd
|
||||
PSHD.3
|
||||
POPD.1
|
||||
STD.1.0 ; new.next = the line that was there
|
||||
BRI linkBeforeAttach
|
||||
|
||||
linkBeforeAtEnd:
|
||||
; Nothing was there, so the new one ends the list and goes after whatever was last.
|
||||
RSTA
|
||||
STA.0
|
||||
INCD.0
|
||||
STA.0
|
||||
SETD.1 NewLine
|
||||
LDD.0.1
|
||||
|
||||
linkBeforeAttach:
|
||||
; And whatever came before now points at the new one. Before the first line, that is
|
||||
; the head of the list rather than a node.
|
||||
SETD.1 PrevLine
|
||||
LDD.2.1
|
||||
PSHD.2
|
||||
POPA
|
||||
POPB
|
||||
OR
|
||||
BRQ linkBeforeHead
|
||||
|
||||
SETD.1 NewLine
|
||||
LDD.0.1
|
||||
SETD.1 PrevLine
|
||||
LDD.1.1
|
||||
STD.0.1
|
||||
RET
|
||||
|
||||
linkBeforeHead:
|
||||
SETD.1 NewLine
|
||||
LDD.0.1
|
||||
SETD.1 TextHead
|
||||
STD.0.1
|
||||
RET
|
||||
|
||||
; A is a line number. Takes it out of the list. Q is zero if there was such a line.
|
||||
unlinkLine:
|
||||
CALL findLine
|
||||
BNQ unlinkMissing
|
||||
|
||||
; What follows the one being taken out.
|
||||
PSHD.3
|
||||
POPD.0
|
||||
LDD.0.0
|
||||
|
||||
SETD.1 PrevLine
|
||||
LDD.2.1
|
||||
PSHD.2
|
||||
POPA
|
||||
POPB
|
||||
OR
|
||||
BRQ unlinkHead
|
||||
|
||||
SETD.1 PrevLine
|
||||
LDD.1.1
|
||||
STD.0.1
|
||||
BRI unlinkDone
|
||||
|
||||
unlinkHead:
|
||||
SETD.1 TextHead
|
||||
STD.0.1
|
||||
|
||||
unlinkDone:
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
unlinkMissing:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; Q is how many lines there are.
|
||||
countLines:
|
||||
RSTA
|
||||
SETD.1 Counted
|
||||
STA.1
|
||||
SETD.1 TextHead
|
||||
LDD.3.1
|
||||
countStep:
|
||||
PSHD.3
|
||||
POPA
|
||||
POPB
|
||||
OR
|
||||
BRQ countDone
|
||||
SETD.1 Counted
|
||||
LDA.1
|
||||
INCA
|
||||
STA.1
|
||||
PSHD.3
|
||||
POPD.0
|
||||
LDD.3.0
|
||||
BRI countStep
|
||||
countDone:
|
||||
SETD.1 Counted
|
||||
LDA.1
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
listLines:
|
||||
INIA 0d1
|
||||
SETD.1 Counted
|
||||
STA.1
|
||||
SETD.1 TextHead
|
||||
LDD.3.1
|
||||
listStep:
|
||||
PSHD.3
|
||||
POPA
|
||||
POPB
|
||||
OR
|
||||
BRQ listDone
|
||||
|
||||
SETD.0 Counted
|
||||
LDA.0
|
||||
CALL printByte
|
||||
SETD.0 ColonText
|
||||
SWI osPrintString
|
||||
|
||||
PSHD.3
|
||||
POPD.1
|
||||
DPUP.1 0d02
|
||||
LDA.1
|
||||
SETD.1 Leftover
|
||||
STA.1
|
||||
PSHD.3
|
||||
POPD.0
|
||||
DPUP.0 0d03
|
||||
SETD.1 Leftover
|
||||
LDA.1
|
||||
BRA listEmpty
|
||||
listChars:
|
||||
LDA.0
|
||||
OUTA 0x00
|
||||
INCD.0
|
||||
SETD.1 Leftover
|
||||
LDA.1
|
||||
DECA
|
||||
STA.1
|
||||
BNA listChars
|
||||
listEmpty:
|
||||
CALL newLine
|
||||
|
||||
SETD.1 Counted
|
||||
LDA.1
|
||||
INCA
|
||||
STA.1
|
||||
PSHD.3
|
||||
POPD.0
|
||||
LDD.3.0
|
||||
BRI listStep
|
||||
listDone:
|
||||
RET
|
||||
|
||||
; ---- The file ----
|
||||
|
||||
; Reads the file into lines, if there is one. A name that is not on the disk is a new
|
||||
; document rather than a mistake, which is what makes this the way to start one.
|
||||
loadFile:
|
||||
SETD.0 FileName
|
||||
SETD.1 0x40 0x00
|
||||
SWI osFileRead
|
||||
BNQ loadNothing
|
||||
|
||||
; How many bytes came back. The service says so in DP3, which is one of the two things a
|
||||
; service is allowed to answer in, and a file that fits in memory has a length that fits
|
||||
; in a pointer. A name that is not on the disk fails here, and that is a new document
|
||||
; rather than a mistake.
|
||||
PSHD.3
|
||||
POPA ; The low byte is on top, the way a pointer is pushed.
|
||||
POPB
|
||||
SETD.1 ReadLeft
|
||||
STB.1
|
||||
INCD.1
|
||||
STA.1
|
||||
|
||||
SETD.0 0x40 0x00
|
||||
SETD.1 Entry
|
||||
RSTA
|
||||
SETD.2 EntryLength
|
||||
STA.2
|
||||
|
||||
splitStep:
|
||||
; Anything left?
|
||||
SETD.2 ReadLeft
|
||||
LDA.2
|
||||
INCD.2
|
||||
LDB.2
|
||||
OR
|
||||
BRQ splitLast
|
||||
|
||||
; How long the line is so far is kept in memory rather than in B, because comparing
|
||||
; against a newline needs B and would quietly count the comparison instead of the line.
|
||||
LDA.0
|
||||
INIB 0d10
|
||||
XOR
|
||||
BRQ splitLine
|
||||
|
||||
STA.1 ; A is still the character; an ALU operation does not touch it.
|
||||
INCD.1
|
||||
SETD.2 EntryLength
|
||||
LDA.2
|
||||
INCA
|
||||
STA.2
|
||||
BRI splitOn
|
||||
|
||||
splitLine:
|
||||
RSTA
|
||||
STA.1 ; The line ends here, so it becomes a string.
|
||||
PSHD.0 ; How far through the file we are.
|
||||
SETD.0 Entry
|
||||
CALL makeNode
|
||||
CALL appendNode
|
||||
POPD.0
|
||||
SETD.1 Entry
|
||||
RSTA
|
||||
SETD.2 EntryLength
|
||||
STA.2
|
||||
|
||||
splitOn:
|
||||
INCD.0
|
||||
SETD.2 ReadLeft
|
||||
CALL takeOneOff
|
||||
BRI splitStep
|
||||
|
||||
splitLast:
|
||||
; A file that does not end in a newline still has a last line in it.
|
||||
SETD.2 EntryLength
|
||||
LDA.2
|
||||
BRA loadNothing
|
||||
RSTA
|
||||
STA.1
|
||||
SETD.0 Entry
|
||||
CALL makeNode
|
||||
CALL appendNode
|
||||
loadNothing:
|
||||
RET
|
||||
|
||||
; DP3 is a node. Puts it on the end of the list.
|
||||
appendNode:
|
||||
; The node has to be put somewhere safe first: counting the lines walks the list in DP3,
|
||||
; which is where the node being added is being held.
|
||||
PSHD.3
|
||||
CALL countLines
|
||||
MVQA
|
||||
INCA
|
||||
POPD.3
|
||||
CALL linkBefore
|
||||
RET
|
||||
|
||||
; DP2 is a two byte count. Takes one off it.
|
||||
takeOneOff:
|
||||
DPUP.2 0d01
|
||||
LDA.2
|
||||
DECA
|
||||
STA.2
|
||||
BNC takeOneDone ; No borrow, so the high half is untouched.
|
||||
DPDN.2 0d01
|
||||
LDA.2
|
||||
DECA
|
||||
STA.2
|
||||
RET
|
||||
takeOneDone:
|
||||
RET
|
||||
|
||||
; Builds the whole document at 0x4000 and saves it. Q is zero if it worked.
|
||||
writeFile:
|
||||
SETD.1 0x40 0x00
|
||||
SETD.2 TextHead
|
||||
LDD.3.2
|
||||
|
||||
writeStep:
|
||||
PSHD.3
|
||||
POPA
|
||||
POPB
|
||||
OR
|
||||
BRQ writeOut
|
||||
|
||||
PSHD.3
|
||||
POPD.0
|
||||
DPUP.0 0d02
|
||||
LDA.0
|
||||
SETD.2 Leftover
|
||||
STA.2
|
||||
INCD.0
|
||||
LDA.2
|
||||
BRA writeBreak
|
||||
writeChars:
|
||||
LDA.0
|
||||
STA.1
|
||||
INCD.0
|
||||
INCD.1
|
||||
SETD.2 Leftover
|
||||
LDA.2
|
||||
DECA
|
||||
STA.2
|
||||
BNA writeChars
|
||||
writeBreak:
|
||||
INIA 0d10
|
||||
STA.1
|
||||
INCD.1
|
||||
|
||||
PSHD.3
|
||||
POPD.0
|
||||
LDD.3.0
|
||||
BRI writeStep
|
||||
|
||||
writeOut:
|
||||
; Where the building stopped says how big it is, with no arithmetic worth the name: the
|
||||
; buffer starts on a page boundary at 0x4000, so the high byte less 0x40 is the number of
|
||||
; whole blocks and the low byte is the tail.
|
||||
SETD.2 WroteSize
|
||||
STD.1.2
|
||||
|
||||
; Turn where it stopped into how big it is, which is one subtraction: the high byte less
|
||||
; 0x40 and the low byte as it stands.
|
||||
SETD.0 WroteSize
|
||||
LDA.0
|
||||
INIB 0x40
|
||||
CCF
|
||||
SUB
|
||||
MVQA
|
||||
SETD.0 WroteSize
|
||||
STA.0 ; WroteSize is now a count of bytes, which is what gets printed.
|
||||
|
||||
; And into the two registers the service takes a size in.
|
||||
SETD.0 WroteSize
|
||||
LDA.0
|
||||
INCD.0
|
||||
LDB.0
|
||||
|
||||
SETD.0 FileName
|
||||
SETD.1 0x40 0x00
|
||||
SWI osFileSave
|
||||
RET
|
||||
|
||||
; ---- All that is left of a console library ----
|
||||
;
|
||||
; Printing a string and reading a line are services now. These three are only here because
|
||||
; what a service takes is not quite what the call sites have: a number arrives in two
|
||||
; registers rather than one or in memory, and a line feed is a string like any other.
|
||||
|
||||
newLine:
|
||||
SETD.0 Break
|
||||
SWI osPrintString
|
||||
RET
|
||||
|
||||
; A is a byte.
|
||||
printByte:
|
||||
PSHA
|
||||
POPB
|
||||
RSTA
|
||||
SWI osPrintNumber
|
||||
RET
|
||||
|
||||
; DP0 is a two byte number, most significant first.
|
||||
printWord:
|
||||
LDA.0
|
||||
INCD.0
|
||||
LDB.0
|
||||
SWI osPrintNumber
|
||||
RET
|
||||
|
||||
; DP0 is a two byte number, A is a byte. Adds the one to the other.
|
||||
addByteToWord:
|
||||
DPUP.0 0d01
|
||||
LDB.0
|
||||
CCF
|
||||
ADD
|
||||
STQ.0
|
||||
DPDN.0 0d01
|
||||
LDA.0
|
||||
RSTB
|
||||
ADD
|
||||
STQ.0
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x1000
|
||||
|
||||
Break:
|
||||
0x0A 0x00
|
||||
PromptText:
|
||||
"> "
|
||||
EnteringText:
|
||||
": "
|
||||
ColonText:
|
||||
": "
|
||||
CommaText:
|
||||
", "
|
||||
LinesText:
|
||||
" lines"
|
||||
LineText:
|
||||
" line"
|
||||
WrittenText:
|
||||
"written, "
|
||||
BytesText:
|
||||
" bytes"
|
||||
DotText:
|
||||
"."
|
||||
WhatText:
|
||||
"l list, a add, i insert, c change, d delete, w write, q quit"
|
||||
NoNameText:
|
||||
"edit what? try: run edit <file>"
|
||||
NoLineText:
|
||||
"there is no such line"
|
||||
NeedsLineText:
|
||||
"which line?"
|
||||
NoWriteText:
|
||||
"it would not write"
|
||||
|
||||
FileName:
|
||||
#Reserve 0d24
|
||||
Command:
|
||||
#Reserve 0d41
|
||||
Entry:
|
||||
#Reserve 0d81
|
||||
|
||||
TextHead:
|
||||
0x00 0x00
|
||||
ArenaFree:
|
||||
0x00 0x00
|
||||
PrevLine:
|
||||
0x00 0x00
|
||||
NewLine:
|
||||
0x00 0x00
|
||||
Wanted:
|
||||
0x00
|
||||
Wanted2:
|
||||
0x00
|
||||
Counted:
|
||||
0x00
|
||||
Leftover:
|
||||
0x00
|
||||
EntryLength:
|
||||
0x00
|
||||
ReadLeft:
|
||||
0x00 0x00
|
||||
WroteSize:
|
||||
0x00 0x00
|
||||
|
||||
#Include text.asm
|
||||
@@ -0,0 +1,176 @@
|
||||
; A program that keeps a file without knowing how a filesystem works.
|
||||
;
|
||||
; INCLUDES NOTHING BUT THE SERVICE NAMES. No sbfs.asm, no console.asm - the system has both
|
||||
; of those running already, and this asks it rather than carrying a second copy. That is the
|
||||
; whole point of the program: if it works, a tool that edits documents does not need two and
|
||||
; a half kilobytes of filesystem bound into it.
|
||||
;
|
||||
; It writes a file, reads it back, says how big it was, renames it, and takes it away again,
|
||||
; which is every file service there is.
|
||||
;
|
||||
; ---- What you will see before the handlers are written ----
|
||||
;
|
||||
; The numbers are pinned but nothing answers to them yet, so the first call dispatches
|
||||
; through an empty vector and the machine stops:
|
||||
;
|
||||
; Fault: Software vector 21, dispatched from Program Address 0x200B, has no handler
|
||||
; installed.
|
||||
;
|
||||
; That is the fault working properly rather than the program being broken. A vector with
|
||||
; nothing in it is not a jump to address zero; it is a stop, with the vector named and the
|
||||
; place it was called from named, which is as much as the machine can know.
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x2000
|
||||
|
||||
start:
|
||||
; ---- Write it ----
|
||||
;
|
||||
; A and B together are how many bytes there are, most significant first, which is the
|
||||
; same sixteen bits a length always is on this machine.
|
||||
SETD.0 Name
|
||||
SETD.1 Body
|
||||
RSTA
|
||||
INIB 0d22 ; The text and its newline. NOT the zero the assembler put
|
||||
SWI osFileSave ; after it: a text file ends where the text ends.
|
||||
BNQ noSave
|
||||
SETD.0 SavedText
|
||||
SWI osPrintString
|
||||
|
||||
; ---- Read it back ----
|
||||
;
|
||||
; DP3 comes back holding how many bytes there were, because that is one of the two things
|
||||
; a service is allowed to answer in and a file that fits in memory has a length that fits
|
||||
; in a pointer.
|
||||
SETD.0 Name
|
||||
SETD.1 Landing
|
||||
SWI osFileRead
|
||||
BNQ noRead
|
||||
SETD.0 ReadText
|
||||
SWI osPrintString
|
||||
|
||||
PSHD.3
|
||||
POPB ; The low byte is on top, the way a pointer is pushed.
|
||||
POPA
|
||||
SWI osPrintNumber
|
||||
SETD.0 BytesText
|
||||
SWI osPrintString
|
||||
|
||||
; And now the length is needed for something rather than just reported. What came back is
|
||||
; a file, not a string: nothing on the disk ends in a zero byte, because the entry says
|
||||
; where it stops instead. So a zero goes on the end before it can be printed as one.
|
||||
SETD.1 Landing
|
||||
PSHD.3
|
||||
POPB
|
||||
POPA
|
||||
walkToEnd:
|
||||
BRB atEnd
|
||||
INCD.1
|
||||
DECB
|
||||
BRI walkToEnd
|
||||
atEnd:
|
||||
RSTA
|
||||
STA.1
|
||||
|
||||
SETD.0 Landing
|
||||
SWI osPrintString
|
||||
|
||||
; ---- Call it something else ----
|
||||
SETD.0 Name
|
||||
SETD.1 OtherName
|
||||
SWI osFileRename
|
||||
BNQ noRename
|
||||
SETD.0 RenamedText
|
||||
SWI osPrintString
|
||||
|
||||
; ---- And take it away ----
|
||||
SETD.0 OtherName
|
||||
SWI osFileDelete
|
||||
BNQ noDelete
|
||||
SETD.0 DeletedText
|
||||
SWI osPrintString
|
||||
|
||||
; Reading it now should fail, and a service saying no is not the same as one that is not
|
||||
; there: this comes back with an answer rather than stopping the machine.
|
||||
SETD.0 OtherName
|
||||
SETD.1 Landing
|
||||
SWI osFileRead
|
||||
BRQ stillThere
|
||||
SETD.0 GoneText
|
||||
SWI osPrintString
|
||||
SWI osExit
|
||||
|
||||
stillThere:
|
||||
SETD.0 StillText
|
||||
SWI osPrintString
|
||||
SWI osExit
|
||||
|
||||
noSave:
|
||||
SETD.0 NoSaveText
|
||||
SWI osPrintString
|
||||
SWI osExit
|
||||
noRead:
|
||||
SETD.0 NoReadText
|
||||
SWI osPrintString
|
||||
SWI osExit
|
||||
noRename:
|
||||
SETD.0 NoRenameText
|
||||
SWI osPrintString
|
||||
SWI osExit
|
||||
noDelete:
|
||||
SETD.0 NoDeleteText
|
||||
SWI osPrintString
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x1000
|
||||
|
||||
Name:
|
||||
"kept.txt"
|
||||
OtherName:
|
||||
"moved.txt"
|
||||
|
||||
Body:
|
||||
"a file kept by asking
|
||||
"
|
||||
|
||||
SavedText:
|
||||
"saved it
|
||||
"
|
||||
ReadText:
|
||||
"read it back, "
|
||||
BytesText:
|
||||
" bytes:
|
||||
"
|
||||
RenamedText:
|
||||
"renamed it
|
||||
"
|
||||
DeletedText:
|
||||
"deleted it
|
||||
"
|
||||
GoneText:
|
||||
"and it is gone
|
||||
"
|
||||
StillText:
|
||||
"but it is still there
|
||||
"
|
||||
|
||||
NoSaveText:
|
||||
"it would not save
|
||||
"
|
||||
NoReadText:
|
||||
"it would not read
|
||||
"
|
||||
NoRenameText:
|
||||
"it would not rename
|
||||
"
|
||||
NoDeleteText:
|
||||
"it would not delete
|
||||
"
|
||||
|
||||
Landing:
|
||||
#Reserve 0d256
|
||||
@@ -0,0 +1,101 @@
|
||||
; A loaded program that is interrupted by the console rather than asking it for anything.
|
||||
;
|
||||
; Until the loadable program format could carry vectors, this could not be written. A
|
||||
; handler has to be an address in the vector table, and a program that is not the one the
|
||||
; machine booted from had no way to say what its vectors were, so interrupts belonged to
|
||||
; boot images and every loaded program had to poll. Snake polls for exactly that reason.
|
||||
;
|
||||
; What this shows is the whole path: the assembler writes the vectors into the file, the
|
||||
; system installs them when the program is run, the console raises its line, the handler
|
||||
; runs, and the system takes the vectors back out again when the program gives the machine
|
||||
; back. Nothing in the waiting loop below looks at the console at all.
|
||||
;
|
||||
; The vector is named by port, because a device interrupts on the port it is plugged into
|
||||
; and the console is on port 0x00.
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x2000
|
||||
|
||||
start:
|
||||
CIF ; Nothing arrives until there is something to catch it.
|
||||
|
||||
; Set rather than trusted to be zero. Running a program a second time does not load it
|
||||
; again, so its Data Segment is exactly as the last run left it - and the last thing the
|
||||
; last run did was set this.
|
||||
RSTA
|
||||
SETD.0 Stopping
|
||||
STA.0
|
||||
|
||||
SETD.0 Banner
|
||||
CALL printString
|
||||
CALL newLine
|
||||
|
||||
; Key mode and interrupt on input, in one write, since the two control bits are
|
||||
; independent of each other.
|
||||
INIA 0x03
|
||||
OUTA 0x02
|
||||
SIF
|
||||
|
||||
wait:
|
||||
; This loop is the point. It never touches the console, so every character that appears
|
||||
; below was put there by something that interrupted it.
|
||||
SETD.3 Stopping
|
||||
LDA.3
|
||||
RSTB
|
||||
OR
|
||||
BRQ wait
|
||||
|
||||
CALL newLine
|
||||
RSTA
|
||||
OUTA 0x02 ; Line mode and no interrupts, the way it was found.
|
||||
SETD.0 DoneText
|
||||
CALL printString
|
||||
CALL newLine
|
||||
SWI osExit
|
||||
|
||||
; Entered because the console had something to say. Never called.
|
||||
keyHandler:
|
||||
INA 0x01
|
||||
INIB 0x01 ; READY: is there a byte, as opposed to the end of input?
|
||||
AND
|
||||
BRQ keyNoByte
|
||||
INA 0x00
|
||||
|
||||
INIB 0x71 ; q, which is how this program is stopped.
|
||||
XOR
|
||||
BRQ keyStop
|
||||
|
||||
OUTA 0x00 ; Nothing echoes in key mode, so the handler does it.
|
||||
RETI
|
||||
|
||||
keyNoByte:
|
||||
; The end of input raises the line once as well, so a program driven entirely by
|
||||
; interrupts is told when nothing more is coming instead of waiting for ever.
|
||||
keyStop:
|
||||
SETD.3 Stopping
|
||||
INIA 0x01
|
||||
STA.3
|
||||
RETI
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x1000
|
||||
|
||||
Banner:
|
||||
"keys, by interrupt. q stops."
|
||||
DoneText:
|
||||
"the console has been handed back"
|
||||
|
||||
; The only thing the handler and the loop it interrupts have to say to each other.
|
||||
Stopping:
|
||||
0x00
|
||||
|
||||
#Vectors
|
||||
|
||||
Boot start
|
||||
Device 0x00 keyHandler
|
||||
|
||||
#Include console.asm
|
||||
@@ -0,0 +1,57 @@
|
||||
; A program that is told what to work on.
|
||||
;
|
||||
; Everything loaded before this one did the same thing however it was started, because
|
||||
; there was no way to tell one anything. A tool that edits a document needs to know which
|
||||
; document, and that is the same need a dozen other things will have, so it is a service
|
||||
; rather than something an editor arranges for itself.
|
||||
;
|
||||
; run says it was given nothing
|
||||
; run whatever else says "whatever else"
|
||||
;
|
||||
; The whole rest of the line arrives, spaces and all, rather than a list of words. What
|
||||
; counts as an argument is the program's business; the system's business is handing over
|
||||
; what was typed.
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x2000
|
||||
|
||||
start:
|
||||
SETD.0 Given
|
||||
INIB 0d64
|
||||
SWI osArgument
|
||||
|
||||
SETD.0 Given
|
||||
LDA.0
|
||||
BRA sayNothing
|
||||
|
||||
SETD.0 SaidText
|
||||
SWI osPrintString
|
||||
SETD.0 Given
|
||||
SWI osPrintString
|
||||
BRI sayEnd
|
||||
|
||||
sayNothing:
|
||||
SETD.0 NothingText
|
||||
SWI osPrintString
|
||||
|
||||
sayEnd:
|
||||
SETD.0 NewLine
|
||||
SWI osPrintString
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x1000
|
||||
|
||||
SaidText:
|
||||
"it says: "
|
||||
NothingText:
|
||||
"nothing was said"
|
||||
NewLine:
|
||||
0x0A 0x00
|
||||
|
||||
Given:
|
||||
#Reserve 0d64
|
||||
@@ -9,13 +9,15 @@
|
||||
; console holds the next key until it is asked, so nothing typed between frames is lost,
|
||||
; and a script of moves plays back one move to a frame.
|
||||
;
|
||||
; WHY IT POLLS RATHER THAN INTERRUPTS. The console can raise an interrupt line when a byte
|
||||
; arrives, which is the better shape for a game: the loop would never look at the console
|
||||
; at all. A loaded program cannot use it. Installing a handler means putting an address in
|
||||
; the vector table, and the loadable program format carries only code and data - a program
|
||||
; that is not the one the machine booted from has no way to say what its vectors are. So
|
||||
; interrupts belong to boot images for now, and this asks once a frame, which is what the
|
||||
; machines this one is pretending to be did anyway.
|
||||
; WHY IT POLLS RATHER THAN INTERRUPTS. When this was written a loaded program could not be
|
||||
; interrupted at all: installing a handler means putting an address in the vector table,
|
||||
; and the loadable format carried only code and data, so a program that was not the one the
|
||||
; machine booted from had no way to say what its vectors were. That is no longer true - the
|
||||
; format carries them now, and Keys.asm is the program that shows it.
|
||||
;
|
||||
; This still polls, and now by choice. Asking once a frame is what the machines this one is
|
||||
; pretending to be actually did, it is the shape a game with a frame loop wants anyway, and
|
||||
; having one of each in the same Apps directory is worth more than having two the same.
|
||||
;
|
||||
; THE BOARD IS A PAGE, and that is the whole trick this program turns on. Sixteen by
|
||||
; sixteen is 256 squares, so a square number is a byte, and the board is aligned so that
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
# CosmOS
|
||||
|
||||
## Overview:
|
||||
CosmOS is a small, single-tasking disk operating environment for the SplitBit 8-bit
|
||||
computer. It boots the machine, finds and mounts an SBFS filesystem, provides a command
|
||||
line and memory monitor, loads applications from disk, and takes control back when they
|
||||
finish.
|
||||
|
||||
CosmOS is written entirely in SplitBit assembly. It is closer in scale and purpose to a
|
||||
resident monitor or an early disk operating system than to a modern multitasking OS: one
|
||||
program owns the machine at a time, there is no privilege boundary, and applications are
|
||||
assembled for fixed regions of memory. What it provides is a stable home from which those
|
||||
programs can be found, run, and given services without each one having to boot the machine
|
||||
for itself.
|
||||
|
||||
### Features:
|
||||
- Interactive Shell: Read commands from the SplitBit console and continue until `exit` or
|
||||
the end of input.
|
||||
- SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk.
|
||||
- Loadable Applications: Validate SBEX files, copy their Program and Data segments into
|
||||
the addresses for which they were assembled, and start them at their declared entry
|
||||
point.
|
||||
- Resident Services: Applications can print strings and numbers, read lines, receive their
|
||||
command arguments, read and write files, and return to the shell through named software
|
||||
interrupts.
|
||||
- Application Vectors: Install interrupt vectors carried by a loadable program and
|
||||
restore whatever they replaced when the program exits.
|
||||
- Stack Reclamation: Save the system Stack before launching an application and take it
|
||||
back on exit, so an application need not unwind itself before returning.
|
||||
- Memory Monitor: Inspect and modify Program Memory, Data Memory, and registered
|
||||
device-memory banks through the SplitBit memory controller, disassemble instructions,
|
||||
and begin execution at an address.
|
||||
- Hardware Discovery: Mount the disk through the device registry rather than assuming
|
||||
that one is present at a particular controller bank.
|
||||
- Native Applications: Includes demonstrations, mathematical programs, interactive
|
||||
programs, a game, and a line-oriented text editor.
|
||||
- Reproducible Disk Image: The makefile assembles the system and every application, then
|
||||
constructs a fresh SBFS image containing the resulting executables.
|
||||
|
||||
## Building and Running:
|
||||
|
||||
CosmOS currently lives inside the SplitBit Emulator repository and uses its assembler,
|
||||
emulator, and disk-image tool. From the repository root, build those tools first:
|
||||
|
||||
```sh
|
||||
make
|
||||
```
|
||||
|
||||
Then build CosmOS and all of its applications:
|
||||
|
||||
```sh
|
||||
cd Programs
|
||||
make cosmos
|
||||
```
|
||||
|
||||
Build a fresh SBFS application disk as well:
|
||||
|
||||
```sh
|
||||
make cosmos-disk
|
||||
```
|
||||
|
||||
To boot CosmOS with that disk attached:
|
||||
|
||||
```sh
|
||||
make run-cosmos
|
||||
```
|
||||
|
||||
The generated files are kept under `Programs/build/`:
|
||||
|
||||
- `CosmOS/Source/cosmos.bin` is the bootable CosmOS image.
|
||||
- `CosmOS/Apps/*.sbx` are loadable application images.
|
||||
- `cosmos.img` is the SBFS disk containing those applications.
|
||||
|
||||
The disk is rebuilt from scratch when its applications change, so its contents describe
|
||||
the current source tree rather than accumulating files left by older builds.
|
||||
|
||||
## Shell Commands:
|
||||
|
||||
CosmOS currently provides these built-in commands:
|
||||
|
||||
| Command | Description |
|
||||
| -- | -- |
|
||||
| `dir` | List the files on the mounted disk and their sizes. |
|
||||
| `load <file>` | Read and validate an SBEX application, then place its code and data where its header requests. |
|
||||
| `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. |
|
||||
| `delete <file>` | Remove a file from the filesystem and release its blocks. |
|
||||
| `rename <file> <to>` | Give a file a different name without moving its contents. |
|
||||
| `monitor` | Enter monitor mode, in which the prompt becomes `*` and the commands below are also available. |
|
||||
| `help` | Show the built-in command summary. |
|
||||
| `exit` | Leave monitor mode if in it, and otherwise halt the machine. |
|
||||
|
||||
Monitor mode adds the following. It is a mode rather than a separate program because a
|
||||
loaded application occupies the one region a loaded application is given, so a monitor
|
||||
which was itself an application could never examine another one. The mode persists: an
|
||||
application started with `g` which returns through `osExit` arrives back at the monitor
|
||||
prompt rather than at the shell.
|
||||
|
||||
| Command | Description |
|
||||
| -- | -- |
|
||||
| `x [address]` | Display 64 bytes as hexadecimal and as characters. |
|
||||
| `d [address]` | Disassemble eight instructions. |
|
||||
| `s <address> <byte>...` | Write bytes into the bank being examined, including Program Memory. |
|
||||
| `b <program\|data\|bank>` | Select a memory space or a registered bank number. |
|
||||
| `g <address>` | Begin execution at an address. |
|
||||
|
||||
`x` and `d` share a position, and each leaves it after what it displayed, so either may be
|
||||
given without an address to continue from where the last one stopped.
|
||||
|
||||
For example:
|
||||
|
||||
```text
|
||||
> dir
|
||||
> load Snake.sbx
|
||||
> run
|
||||
```
|
||||
|
||||
Loading and running are separate operations for now. A loaded program may be run again
|
||||
without being read from disk again, which is useful both as a monitor facility and as a
|
||||
test that CosmOS correctly restores its Stack and vector table after every run.
|
||||
|
||||
CosmOS also boots without a disk. It reports that no filesystem was found, leaves the
|
||||
shell and memory monitor available, and refuses commands that require a mounted disk
|
||||
without stopping the machine.
|
||||
|
||||
## Included Applications:
|
||||
|
||||
The application disk is populated from every assembly file in `CosmOS/Apps/`. At present
|
||||
it includes:
|
||||
|
||||
- `Edit`: A line-oriented text editor that can create, load, modify, and save files from
|
||||
inside CosmOS.
|
||||
- `Snake`: A playable terminal game using nonblocking single-key input.
|
||||
- `Life`: A 16 by 16 Conway's Game of Life simulation that returns when it settles or
|
||||
reaches its generation limit.
|
||||
- `Keys`: An interrupt-driven console demonstration carrying its own hardware vector.
|
||||
- `Say`: Demonstrates receiving the argument supplied to `run`.
|
||||
- `Files`: Writes, reads, renames, and deletes a file using only the system's services,
|
||||
including no filesystem code of its own.
|
||||
- `greet` and `hello`: Small examples of, respectively, using CosmOS services and talking
|
||||
directly to SplitBit hardware.
|
||||
- `Fib-8`, `Fib-16`, and `Fib-32`: Fibonacci demonstrations at three integer widths.
|
||||
- `Sieve-8` and `Sieve-16`: Prime sieves covering the 8-bit and 16-bit ranges.
|
||||
|
||||
These programs are ordinary SBEX files on SBFS. They are not built into the operating
|
||||
system, and the host-side `SplitDisk` tool can add or remove other files from an image.
|
||||
|
||||
## The Application Model:
|
||||
|
||||
CosmOS divides the two SplitBit address spaces by convention:
|
||||
|
||||
| Memory | CosmOS | Loaded application |
|
||||
| -- | -- | -- |
|
||||
| Program Memory | `0x0000` through `0x1FFF` | `0x2000` and above |
|
||||
| Data Memory | `0x0000` through `0x0FFF` | `0x1000` and above |
|
||||
|
||||
Applications state their actual Program and Data addresses with `#Base`. The SplitBit
|
||||
assembler then writes an SBEX loadable image containing those addresses, the entry point,
|
||||
the segment lengths, and any vectors the application needs. CosmOS does not relocate
|
||||
code: the addresses in the file must be the addresses for which it was assembled.
|
||||
|
||||
This division is an ABI convention rather than protection. An application owns the
|
||||
machine while it runs and may address hardware or CosmOS memory directly. The convention
|
||||
keeps independently assembled software out of the system's way; it is not a security
|
||||
boundary.
|
||||
|
||||
### System Services:
|
||||
|
||||
Applications include `Source/services.asm` to obtain stable names and vector numbers for
|
||||
the services CosmOS provides. The currently installed services are:
|
||||
|
||||
| Service | Interface |
|
||||
| -- | -- |
|
||||
| `osPrintString` | DP0 names a zero-terminated string to print. |
|
||||
| `osReadLine` | DP0 names a destination and B is its capacity; Q returns the line length. |
|
||||
| `osArgument` | DP0 names a destination and B is its capacity; receives the text following `run`. |
|
||||
| `osExit` | Abandon the application's Stack, restore the CosmOS environment, and return to the shell. |
|
||||
| `osFileRead` | DP0 names a file and DP1 a destination; Q reports success and DP3 returns its length in bytes. |
|
||||
| `osFileSave` | DP0 names a file, DP1 supplies its contents, and A with B give the length; Q reports success. |
|
||||
| `osFileDelete` | DP0 names a file to remove; Q reports success. |
|
||||
| `osFileRename` | DP0 names an existing file and DP1 its new name; Q reports success. |
|
||||
| `osPrintNumber` | A with B give a number to print in decimal without leading zeroes. |
|
||||
|
||||
The filesystem services exist so that an application need not contain a second copy of the
|
||||
filesystem in order to keep a file. There is deliberately no service to mount a disk: the
|
||||
system mounts one before its first prompt, and an application mounting it again was only
|
||||
ever a consequence of owning a private copy of the library.
|
||||
|
||||
A minimal CosmOS application therefore looks like this:
|
||||
|
||||
```asm
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
#Base 0x2000
|
||||
|
||||
start:
|
||||
SETD.0 Message
|
||||
SWI osPrintString
|
||||
SWI osExit
|
||||
|
||||
#Data
|
||||
#Base 0x1000
|
||||
|
||||
Message:
|
||||
"Hello from CosmOS."
|
||||
```
|
||||
|
||||
An application may also include its own libraries or access hardware ports directly.
|
||||
The services are an interface offered by the system, not the only way software is allowed
|
||||
to use the computer.
|
||||
|
||||
## Source Layout:
|
||||
|
||||
- `Source/cosmos.asm`: Boot process, shell, loader, monitor, system services, and
|
||||
application lifecycle.
|
||||
- `Source/console.asm`: Console input, strings, hexadecimal and decimal output, and line
|
||||
handling.
|
||||
- `Source/text.asm`: String comparison, splitting, and hexadecimal text conversion used
|
||||
by the shell.
|
||||
- `Source/sbfs.asm`: Target-side implementation of the SplitBit filesystem.
|
||||
- `Source/services.asm`: The shared names and stable vector numbers used by CosmOS and
|
||||
separately assembled applications.
|
||||
- `Apps/`: Loadable programs packaged onto the CosmOS disk image.
|
||||
|
||||
## Tests:
|
||||
|
||||
CosmOS is exercised as part of the SplitBit repository's normal test suite:
|
||||
|
||||
```sh
|
||||
make test
|
||||
```
|
||||
|
||||
The tests boot the system with and without a disk and drive the shell through recorded
|
||||
console input. They cover directory traversal, every loader refusal, repeated application
|
||||
runs, memory inspection, vector installation and restoration, command arguments,
|
||||
filesystem deletion and renaming, interactive applications, and editing a file followed
|
||||
by reading the saved result back in a second editor session.
|
||||
|
||||
Individual CosmOS tests can be run from the repository root, for example:
|
||||
|
||||
```sh
|
||||
./Tests/run.sh cosmos cosmosRun cosmosEdit
|
||||
```
|
||||
|
||||
Test disks are constructed with the host-side `SplitDisk` tool. CosmOS is therefore
|
||||
reading filesystems written by an independent implementation of the same format rather
|
||||
than merely checking its filesystem code against itself.
|
||||
|
||||
## Current Scope:
|
||||
|
||||
CosmOS is early software for an experimental computer. It runs one application at a
|
||||
time, has no privilege levels or process isolation, does not relocate applications, and
|
||||
does not yet provide a native assembler or linker. Its present purpose is to make
|
||||
SplitBit usable from inside the machine: inspect it, manage persistent files, load
|
||||
programs, provide common services, and return reliably to a command prompt.
|
||||
|
||||
The intended long-term milestone is self-hosting: editing SplitBit assembly source under
|
||||
CosmOS, assembling and linking it natively, and eventually rebuilding CosmOS and its own
|
||||
development tools on the machine.
|
||||
|
||||
## Additional Information:
|
||||
|
||||
The SplitBit Programming Manual describes the CPU, devices, memory controller, SBFS,
|
||||
SBEX format, interrupt model, and CosmOS service interface. The SplitBit Assembler Manual
|
||||
documents the assembly language, loadable-program bases, and vector declarations.
|
||||
|
||||
## License:
|
||||
|
||||
CosmOS is part of the SplitBit Emulator project and is licensed under the Apache License,
|
||||
Version 2.0. See the repository's top-level `LICENSE` file for the full license text.
|
||||
+1282
-56
File diff suppressed because it is too large
Load Diff
@@ -350,13 +350,20 @@ sbfsWalkName:
|
||||
; 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.
|
||||
; A COUNTER OF ITS OWN, and that matters more than it looks. sbfsFind counts the eight
|
||||
; entries of a block in SbfsCount and calls this for each one; when this used SbfsCount too,
|
||||
; a failed comparison left the ENTRY count holding however far the NAME comparison had got.
|
||||
; A name differing at its first byte left 22 there, so the search walked twenty two entries
|
||||
; through a buffer holding eight - off the end of it, into whatever data happened to follow,
|
||||
; and reported a match against rubbish. What followed the buffer decided whether it looked
|
||||
; like it worked, which is why it went unnoticed until unrelated data was added.
|
||||
sbfsMatchEntry:
|
||||
PSHD.2
|
||||
POPD.0
|
||||
DPUP.0 0d06 ; The name inside the entry.
|
||||
SETD.2 SbfsWanted
|
||||
INIA 0d22
|
||||
SETD.1 SbfsCount
|
||||
SETD.1 SbfsMatchLeft
|
||||
STA.1
|
||||
|
||||
sbfsMatchLoop:
|
||||
@@ -991,6 +998,208 @@ sbfsSameByte:
|
||||
XOR
|
||||
RET
|
||||
|
||||
; ---- Deleting ----
|
||||
;
|
||||
; Frees a file. DP0 names it, and Q is zero if it went.
|
||||
;
|
||||
; A deleted entry and one that was never used are the same thing, which is the whole of
|
||||
; what deleting is here: the entry is zeroed and its blocks stop being spoken for. THE
|
||||
; BLOCKS THEMSELVES ARE LEFT EXACTLY AS THEY WERE, so what was in a file is still on the
|
||||
; disk until something is put over the top of it. Worth knowing if anything is ever meant
|
||||
; to be private. The host tool does the same, so the two agree about what a deleted disk
|
||||
; looks like.
|
||||
;
|
||||
; Nothing is compacted. Deleting leaves a hole, and because files are contiguous a hole is
|
||||
; only usable by something that fits inside it. That is the price of the directory being
|
||||
; the whole allocation map, and tidying it up is an ordinary program somebody can write
|
||||
; rather than anything the format has to say.
|
||||
sbfsDelete:
|
||||
CALL sbfsFind
|
||||
BNQ sbfsDeleteFailed
|
||||
|
||||
; How much room it was taking, worked out before the entry that says so is thrown away.
|
||||
CALL sbfsFileExtent
|
||||
|
||||
; sbfsFind leaves DP3 on the entry and SbfsBlock on the directory block it came out of,
|
||||
; which is everything needed to change it and put it back.
|
||||
PSHD.3
|
||||
POPD.0
|
||||
INIB 0d32
|
||||
sbfsDeleteWipe:
|
||||
RSTA
|
||||
STA.0
|
||||
INCD.0
|
||||
DECB
|
||||
BNB sbfsDeleteWipe
|
||||
|
||||
SETD.1 SbfsBuffer
|
||||
CALL sbfsBufferIn
|
||||
CALL sbfsWriteBlock
|
||||
BNQ sbfsDeleteFailed
|
||||
|
||||
; And the free count goes back up. It is a note rather than the truth, but a note worth
|
||||
; keeping right.
|
||||
RSTA
|
||||
SETD.0 SbfsBlock
|
||||
STA.0
|
||||
INCD.0
|
||||
STA.0
|
||||
CALL sbfsReadBlock
|
||||
BNQ sbfsDeleteFailed
|
||||
SETD.1 SbfsBuffer
|
||||
CALL sbfsBufferOut
|
||||
SETD.0 SbfsBuffer
|
||||
DPUP.0 0d12
|
||||
SETD.2 SbfsWantBlocks
|
||||
CALL sbfsAddWord
|
||||
SETD.1 SbfsBuffer
|
||||
CALL sbfsBufferIn
|
||||
CALL sbfsWriteBlock
|
||||
RET
|
||||
|
||||
sbfsDeleteFailed:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; ---- Renaming ----
|
||||
;
|
||||
; DP0 is the name a file has, DP1 is the name it should have. Q is zero if it was renamed.
|
||||
;
|
||||
; Only the twenty two bytes of the name change, so no data moves and no block is touched
|
||||
; but the one holding the entry. THAT IS WHAT MAKES A SAFE SAVE POSSIBLE. Writing a file
|
||||
; that has grown means putting it somewhere else, and the obvious order - throw the old one
|
||||
; away, then write the new one - loses the lot if there turns out to be nowhere to put it.
|
||||
; Renaming is the cheapest thing this filesystem can do and the only one that can be left
|
||||
; until last, so it is what the order is built around.
|
||||
sbfsRename:
|
||||
; Where the old name is, kept somewhere that finding things will not tread on.
|
||||
SETD.2 SbfsSavedName
|
||||
STD.0.2
|
||||
|
||||
PSHD.1
|
||||
POPD.0
|
||||
SETD.1 SbfsNewName
|
||||
CALL sbfsKeepName ; Twenty two bytes, padded, the way an entry holds one.
|
||||
|
||||
; Refused if something already answers to the new name. Two entries with one name is a
|
||||
; disk that cannot be searched sensibly: a search answers with whichever it meets first,
|
||||
; so the other becomes unreachable without ever having been deleted.
|
||||
SETD.0 SbfsNewName
|
||||
CALL sbfsFind
|
||||
BRQ sbfsRenameFailed
|
||||
|
||||
SETD.2 SbfsSavedName
|
||||
LDD.0.2
|
||||
CALL sbfsFind
|
||||
BNQ sbfsRenameFailed
|
||||
|
||||
PSHD.3
|
||||
POPD.1
|
||||
DPUP.1 0d06 ; Past the flags, the start, the block count and the tail.
|
||||
SETD.0 SbfsNewName
|
||||
INIA 0d22
|
||||
SETD.2 SbfsCount
|
||||
STA.2
|
||||
sbfsRenameName:
|
||||
LDA.0
|
||||
STA.1
|
||||
INCD.0
|
||||
INCD.1
|
||||
LDA.2
|
||||
DECA
|
||||
STA.2
|
||||
BNA sbfsRenameName
|
||||
|
||||
SETD.1 SbfsBuffer
|
||||
CALL sbfsBufferIn
|
||||
CALL sbfsWriteBlock
|
||||
RET
|
||||
|
||||
sbfsRenameFailed:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; ---- Saving over something that is already there ----
|
||||
;
|
||||
; DP0 names the file, DP1 is the data, and SbfsFileBlocks with SbfsFileTail say how big it
|
||||
; now is. Q is zero if it was saved.
|
||||
;
|
||||
; This is the routine every tool that edits a document wants, and the reason it is here
|
||||
; rather than in each of them is that the careful order is not obvious and getting it wrong
|
||||
; destroys somebody's work:
|
||||
;
|
||||
; make a temporary nothing is lost if there is nowhere to put it
|
||||
; write it
|
||||
; delete the original only once the new one is safely down
|
||||
; rename the temporary
|
||||
;
|
||||
; The obvious order - delete, create, write - looks fine and is a trap. Files here are
|
||||
; contiguous, so a file that has grown may not fit where it was, and a create can be
|
||||
; refused for want of a run long enough even on a disk with plenty of free blocks. Do it
|
||||
; that way round and the original is already gone when that happens.
|
||||
sbfsSaveFile:
|
||||
SETD.2 SbfsSaveName
|
||||
STD.0.2
|
||||
SETD.2 SbfsSaveData
|
||||
STD.1.2
|
||||
|
||||
; How big it is, kept aside: finding and deleting things both overwrite the place the
|
||||
; size is normally said, because both of them describe whatever they last looked at.
|
||||
SETD.0 SbfsSaveBlocks
|
||||
SETD.2 SbfsFileBlocks
|
||||
CALL sbfsSetWord
|
||||
SETD.0 SbfsFileTail
|
||||
LDA.0
|
||||
SETD.1 SbfsSaveTail
|
||||
STA.1
|
||||
|
||||
; A temporary left behind by a save that did not finish would be in the way. Whether
|
||||
; there was one is not worth asking about, since either answer leads here.
|
||||
SETD.0 SbfsTempName
|
||||
CALL sbfsDelete
|
||||
|
||||
SETD.0 SbfsFileBlocks
|
||||
SETD.2 SbfsSaveBlocks
|
||||
CALL sbfsSetWord
|
||||
SETD.0 SbfsSaveTail
|
||||
LDA.0
|
||||
SETD.1 SbfsFileTail
|
||||
STA.1
|
||||
|
||||
SETD.0 SbfsTempName
|
||||
CALL sbfsCreate
|
||||
BNQ sbfsSaveFailed
|
||||
|
||||
SETD.2 SbfsSaveData
|
||||
LDD.1.2
|
||||
CALL sbfsWriteFile
|
||||
BNQ sbfsSaveFailed
|
||||
|
||||
; Now, and not before, the old one goes. It may not exist, which is what saving something
|
||||
; for the first time looks like from here.
|
||||
SETD.2 SbfsSaveName
|
||||
LDD.0.2
|
||||
CALL sbfsDelete
|
||||
|
||||
SETD.0 SbfsTempName
|
||||
SETD.2 SbfsSaveName
|
||||
LDD.1.2
|
||||
CALL sbfsRename
|
||||
RET
|
||||
|
||||
sbfsSaveFailed:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
SbfsMagic:
|
||||
@@ -1026,6 +1235,8 @@ SbfsOne:
|
||||
0x00 0x01
|
||||
SbfsCount:
|
||||
0x00
|
||||
SbfsMatchLeft:
|
||||
0x00
|
||||
SbfsLeft:
|
||||
0x00
|
||||
|
||||
@@ -1050,5 +1261,29 @@ SbfsName:
|
||||
SbfsWanted:
|
||||
#Reserve 0d22
|
||||
|
||||
; ---- What renaming and saving keep ----
|
||||
|
||||
; A name on its way into an entry, padded to the twenty two bytes an entry holds.
|
||||
SbfsNewName:
|
||||
#Reserve 0d22
|
||||
|
||||
; Where a name lives, kept across a search, which needs the pointers for itself.
|
||||
SbfsSavedName:
|
||||
0x00 0x00
|
||||
|
||||
SbfsSaveName:
|
||||
0x00 0x00
|
||||
SbfsSaveData:
|
||||
0x00 0x00
|
||||
SbfsSaveBlocks:
|
||||
0x00 0x00
|
||||
SbfsSaveTail:
|
||||
0x00
|
||||
|
||||
; What a document is called while it is being written and is not yet the real thing. A
|
||||
; name nothing else is likely to want, and short enough to leave room for a long one.
|
||||
SbfsTempName:
|
||||
"sbfs.part"
|
||||
|
||||
SbfsBuffer:
|
||||
#Reserve 0d256
|
||||
|
||||
@@ -24,3 +24,30 @@
|
||||
osPrintString 0d16 ; DP0 names a string. Prints it.
|
||||
osReadLine 0d17 ; DP0 names somewhere to put a line read from the console.
|
||||
osExit 0d18 ; Give the machine back to the system.
|
||||
osArgument 0d19 ; DP0 names somewhere to put the rest of the run command.
|
||||
|
||||
; ---- What the system does with the disk on a program's behalf ----
|
||||
;
|
||||
; A loaded program that wanted a file used to include the whole filesystem, which is two
|
||||
; and a half kilobytes of it carrying a private copy of code the system already has
|
||||
; running. These are that code, reachable.
|
||||
;
|
||||
; NOTHING HERE MOUNTS ANYTHING. The system mounted the disk before it read the prompt, and
|
||||
; there is one disk with one buffer registered as one bank; a program mounting it again was
|
||||
; only ever an artefact of having its own copy of the library.
|
||||
;
|
||||
; Sizes are in bytes and fit the registers exactly. A file that can be read into Data
|
||||
; Memory is under 64K by definition, so its length is sixteen bits: coming back it is DP3,
|
||||
; going out it is A and B together, and neither direction needs a record in memory that
|
||||
; both sides have to agree on the shape of.
|
||||
osFileRead 0d20 ; DP0 names it, DP1 says where. Q is zero if it read, DP3 is how many bytes.
|
||||
osFileSave 0d21 ; DP0 names it, DP1 is the bytes, A and B are how many. Q is zero if it saved.
|
||||
osFileDelete 0d22 ; DP0 names it. Q is zero if it went.
|
||||
osFileRename 0d23 ; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved.
|
||||
|
||||
; ---- And with the console ----
|
||||
;
|
||||
; printString is already up there. This is the other half of what a program prints: a
|
||||
; number, in decimal, without leading zeroes. A and B together, so one service covers both
|
||||
; a line number and a byte count and there is no need for two.
|
||||
osPrintNumber 0d24
|
||||
|
||||
@@ -215,6 +215,82 @@ textHexNo:
|
||||
ADD
|
||||
RET
|
||||
|
||||
; ---- A number written in decimal ----
|
||||
;
|
||||
; DP0 names it. Q is the value, and TextDigits says how many digits were read, which is
|
||||
; zero when there was no number there at all. Stops at the first thing that is not a digit.
|
||||
;
|
||||
; Decimal rather than hex, and one byte rather than two, because this is for the numbers a
|
||||
; person types at a program: a line number, a count, a how many. Nobody counts lines in
|
||||
; hex, and nobody types a line number above 255 on a machine this size. textHexWord is
|
||||
; still the one for an address, where hex is what everybody means.
|
||||
;
|
||||
; Ten times the running total is worked out as eight of it plus two of it, because nothing
|
||||
; on this machine multiplies. Anything past 255 wraps, which is what the same sum does
|
||||
; everywhere else here.
|
||||
textNumber:
|
||||
RSTA
|
||||
SETD.1 TextValue
|
||||
STA.1
|
||||
SETD.1 TextDigits
|
||||
STA.1
|
||||
|
||||
textNumberLoop:
|
||||
LDA.0
|
||||
BRA textNumberDone
|
||||
|
||||
; Below '0' or above '9' ends it.
|
||||
INIB 0d48
|
||||
CCF
|
||||
SUB
|
||||
BRC textNumberDone ; It borrowed, so the character was below '0'.
|
||||
MVQA
|
||||
INIB 0d10
|
||||
CCF
|
||||
SUB
|
||||
BNC textNumberDone ; It did not borrow, so it was ten or more past '0'.
|
||||
|
||||
PSHA ; The digit, while the total is multiplied.
|
||||
SETD.1 TextValue
|
||||
LDA.1
|
||||
LDB.1
|
||||
CCF
|
||||
ADD ; Twice.
|
||||
MVQA
|
||||
MVQB
|
||||
PSHA ; Twice, kept: ten is eight and two.
|
||||
CCF
|
||||
ADD ; Four times.
|
||||
MVQA
|
||||
MVQB
|
||||
CCF
|
||||
ADD ; Eight times.
|
||||
MVQA
|
||||
POPB
|
||||
CCF
|
||||
ADD ; Ten times.
|
||||
MVQA
|
||||
POPB
|
||||
CCF
|
||||
ADD ; And the digit.
|
||||
SETD.1 TextValue
|
||||
STQ.1
|
||||
|
||||
SETD.1 TextDigits
|
||||
LDA.1
|
||||
INCA
|
||||
STA.1
|
||||
INCD.0
|
||||
BRI textNumberLoop
|
||||
|
||||
textNumberDone:
|
||||
SETD.1 TextValue
|
||||
LDA.1
|
||||
RSTB
|
||||
CCF
|
||||
ADD ; Q is the value, the way a routine hands a byte back.
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
; Where the rest of the line begins, after textSplit has taken a word off the front.
|
||||
|
||||
+5
-2
@@ -76,10 +76,13 @@ cosmos: $(COSMOS) $(APPS)
|
||||
$(COSMOS_DISK): $(APPS)
|
||||
@mkdir -p $(@D)
|
||||
rm -f $@
|
||||
$(DISKTOOL) format $@ 64 2
|
||||
$(DISKTOOL) format $@ 256 2
|
||||
@for app in $(APPS); do $(DISKTOOL) put $@ $$app; done
|
||||
|
||||
cosmos-disk: $(COSMOS_DISK)
|
||||
# The system as well as the disk. Building only the image leaves whatever cosmos.bin was
|
||||
# there before, or none at all, and then the disk is booted with a system that does not
|
||||
# match the programs on it.
|
||||
cosmos-disk: $(COSMOS) $(COSMOS_DISK)
|
||||
|
||||
run-cosmos: $(COSMOS) $(COSMOS_DISK)
|
||||
$(EMU) --disk $(COSMOS_DISK) $(COSMOS)
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
; Deleting, renaming, and saving over something that is already there.
|
||||
;
|
||||
; Reading and writing were built first because a program had to be got onto a disk and off
|
||||
; it again. What a document needs is different and it is all about the second time: a file
|
||||
; that is written once is easy, and a file that is written again having grown is where this
|
||||
; format's bargain shows. Files are contiguous and do not grow, so saving a longer version
|
||||
; means putting it somewhere else and letting go of where it was.
|
||||
;
|
||||
; THE ORDER MATTERS AND THE OBVIOUS ONE IS WRONG. Delete the old, create the new, write it:
|
||||
; that loses the lot when the create is refused for want of a run long enough, which is a
|
||||
; thing that happens on a disk with plenty of free blocks once it is in pieces. sbfsSaveFile
|
||||
; does it the other way round, and the rename at the end of that is why renaming exists.
|
||||
;
|
||||
; Correct output is:
|
||||
; here.txt 0002 already here
|
||||
; doc.txt 0003 first draft
|
||||
; doc.txt 0004 a second draft, which is longer than the first
|
||||
; notes.txt 0004 a second draft, which is longer than the first
|
||||
; doc.txt gone
|
||||
; notes.txt gone
|
||||
;
|
||||
; The start block moving from 0003 to 0004 is the file being put somewhere else, which is
|
||||
; what saving a longer one has to do. The last two lines are a rename and a delete having
|
||||
; actually happened rather than having been reported.
|
||||
|
||||
#Include print.asm
|
||||
#Include sbfs.asm
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
CALL sbfsMount
|
||||
BNQ failed
|
||||
|
||||
SETD.0 Existing
|
||||
CALL report
|
||||
BNQ failed
|
||||
|
||||
; Written once, the ordinary way.
|
||||
SETD.0 DocName
|
||||
SETD.1 ShortText
|
||||
INIA 0d11
|
||||
CALL makeAndWrite
|
||||
BNQ failed
|
||||
SETD.0 DocName
|
||||
CALL report
|
||||
BNQ failed
|
||||
|
||||
; And again, longer. Nothing here says where it goes; that is sbfsSaveFile's business.
|
||||
SETD.0 SbfsFileBlocks
|
||||
RSTA
|
||||
STA.0
|
||||
INCD.0
|
||||
STA.0
|
||||
SETD.0 SbfsFileTail
|
||||
INIA 0d46
|
||||
STA.0
|
||||
SETD.0 DocName
|
||||
SETD.1 LongText
|
||||
CALL sbfsSaveFile
|
||||
BNQ failed
|
||||
SETD.0 DocName
|
||||
CALL report
|
||||
BNQ failed
|
||||
|
||||
; Renaming moves nothing: the same blocks answer to a different name.
|
||||
SETD.0 DocName
|
||||
SETD.1 NewName
|
||||
CALL sbfsRename
|
||||
BNQ failed
|
||||
SETD.0 NewName
|
||||
CALL report
|
||||
BNQ failed
|
||||
|
||||
; And the old name is not there any more, which is the half of renaming that could have
|
||||
; quietly not happened.
|
||||
SETD.0 DocName
|
||||
CALL expectGone
|
||||
|
||||
SETD.0 NewName
|
||||
CALL sbfsDelete
|
||||
BNQ failed
|
||||
SETD.0 NewName
|
||||
CALL expectGone
|
||||
HALT
|
||||
|
||||
failed:
|
||||
SETD.0 Failed
|
||||
CALL printString
|
||||
CALL lineFeed
|
||||
HALT
|
||||
|
||||
; DP0 names the file, DP1 is its text, A is how long it is. Everything here fits in a
|
||||
; block, so the whole length is the tail.
|
||||
makeAndWrite:
|
||||
PSHD.1
|
||||
SETD.1 SbfsFileTail
|
||||
STA.1
|
||||
RSTA
|
||||
SETD.1 SbfsFileBlocks
|
||||
STA.1
|
||||
INCD.1
|
||||
STA.1
|
||||
CALL sbfsCreate
|
||||
POPD.1
|
||||
BNQ makeFailed
|
||||
CALL sbfsWriteFile
|
||||
RET
|
||||
makeFailed:
|
||||
RET
|
||||
|
||||
; DP0 names a file that should not be there. Says so either way.
|
||||
expectGone:
|
||||
PSHD.0
|
||||
POPD.3
|
||||
CALL printString
|
||||
CALL padName
|
||||
PSHD.3
|
||||
POPD.0
|
||||
CALL sbfsFind
|
||||
BRQ goneStillThere
|
||||
SETD.0 GoneText
|
||||
CALL printString
|
||||
CALL lineFeed
|
||||
RET
|
||||
goneStillThere:
|
||||
SETD.0 StillText
|
||||
CALL printString
|
||||
CALL lineFeed
|
||||
RET
|
||||
|
||||
; Prints a file's name, where it begins, and what is in it.
|
||||
report:
|
||||
PSHD.0
|
||||
POPD.3
|
||||
CALL printString
|
||||
CALL padName
|
||||
PSHD.3
|
||||
POPD.0
|
||||
CALL sbfsFind
|
||||
BNQ reportFailed
|
||||
|
||||
SETD.0 SbfsFileStart
|
||||
LDA.0
|
||||
CALL printByteHex
|
||||
INCD.0
|
||||
LDA.0
|
||||
CALL printByteHex
|
||||
CALL blankSpace
|
||||
|
||||
SETD.1 Landing
|
||||
CALL sbfsRead
|
||||
BNQ reportFailed
|
||||
|
||||
SETD.0 Landing
|
||||
SETD.1 SbfsFileTail
|
||||
LDA.1
|
||||
SETD.1 LeftOver
|
||||
STA.1
|
||||
BRA reportEnd
|
||||
reportLoop:
|
||||
LDA.0
|
||||
OUTA 0x00
|
||||
INCD.0
|
||||
SETD.1 LeftOver
|
||||
LDA.1
|
||||
DECA
|
||||
STA.1
|
||||
BNA reportLoop
|
||||
reportEnd:
|
||||
CALL lineFeed
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
reportFailed:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; Names are different lengths and the columns should not be, so this pads out to eleven.
|
||||
; DP3 holds the name, which is where the caller left it.
|
||||
padName:
|
||||
PSHD.3
|
||||
POPD.0
|
||||
INIB 0d11
|
||||
padCount:
|
||||
LDA.0
|
||||
BRA padOut
|
||||
INCD.0
|
||||
DECB
|
||||
BNB padCount
|
||||
padOut:
|
||||
RSTA
|
||||
padLoop:
|
||||
BRB padDone
|
||||
INIA 0x20
|
||||
OUTA 0x00
|
||||
DECB
|
||||
BRI padLoop
|
||||
padDone:
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
Existing:
|
||||
"here.txt"
|
||||
DocName:
|
||||
"doc.txt"
|
||||
NewName:
|
||||
"notes.txt"
|
||||
|
||||
ShortText:
|
||||
"first draft"
|
||||
LongText:
|
||||
"a second draft, which is longer than the first"
|
||||
|
||||
GoneText:
|
||||
"gone"
|
||||
StillText:
|
||||
"STILL THERE"
|
||||
Failed:
|
||||
"failed"
|
||||
|
||||
LeftOver:
|
||||
0x00
|
||||
|
||||
Landing:
|
||||
#Reserve 0d256
|
||||
@@ -0,0 +1,107 @@
|
||||
; A software interrupt handing something back.
|
||||
;
|
||||
; RETI restores every register from the frame, which is what makes an interrupt safe to
|
||||
; arrive at an arbitrary moment: the interrupted code cannot tell it happened. A SERVICE is
|
||||
; not arbitrary - it was asked for - and the same rule means it has no way to answer.
|
||||
;
|
||||
; So a service that has something to say writes it INTO ITS OWN FRAME, over the saved
|
||||
; register, and lets RETI put it back. MVSD is what makes the frame reachable: it copies
|
||||
; the Stack Pointer into a Data Pointer, and the frame sits just above it.
|
||||
;
|
||||
; +1 Status +5 DP3 high +9 DP1 high +13 resume high
|
||||
; +2 Q +6 DP3 low +10 DP1 low +14 resume low
|
||||
; +3 A +7 DP2 high +11 DP0 high
|
||||
; +4 B +8 DP2 low +12 DP0 low
|
||||
;
|
||||
; WHICH REGISTERS A SERVICE MAY ANSWER IN is a convention rather than a rule, and it is the
|
||||
; same one CALL already has: Q and DP3. A subroutine cannot hand back A, B or DP0 to DP2
|
||||
; because RET puts them back; a service could write over any of them, and should not, for
|
||||
; exactly the reason the first list exists. A caller expects what it kept to still be there.
|
||||
;
|
||||
; ONLY THE HANDLER ITSELF CAN DO THIS. The offsets are from where the Stack Pointer is, and
|
||||
; a CALL moves it by ten. A routine called by a handler that tried this would be writing
|
||||
; into its own return address.
|
||||
;
|
||||
; Correct output is:
|
||||
; quiet: 7 a service that says nothing leaves Q as it found it
|
||||
; answer: 42 one that does, does not
|
||||
; pointer: ABC and DP3 comes back the same way
|
||||
|
||||
#Include console.asm
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
; Something recognisable in Q, so that a service leaving it alone is visible.
|
||||
INIA 0d7
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
|
||||
SETD.0 QuietText
|
||||
CALL printString
|
||||
SWI quiet
|
||||
MVQA
|
||||
CALL printByteDecimal
|
||||
CALL newLine
|
||||
|
||||
SETD.0 AnswerText
|
||||
CALL printString
|
||||
SWI answer
|
||||
MVQA
|
||||
CALL printByteDecimal
|
||||
CALL newLine
|
||||
|
||||
SETD.0 PointerText
|
||||
CALL printString
|
||||
SWI pointer
|
||||
PSHD.3
|
||||
POPD.0
|
||||
CALL printString
|
||||
CALL newLine
|
||||
HALT
|
||||
|
||||
; Says nothing, so whatever the caller had in Q is still there afterwards.
|
||||
quiet:
|
||||
INIA 0d99
|
||||
RSTB
|
||||
CCF
|
||||
ADD ; Q is 99 in here, and nobody outside will ever know.
|
||||
RETI
|
||||
|
||||
answer:
|
||||
INIA 0d42
|
||||
MVSD.1
|
||||
DPUP.1 0d02 ; The saved Q.
|
||||
STA.1
|
||||
RETI
|
||||
|
||||
pointer:
|
||||
SETD.0 Letters
|
||||
MVSD.1
|
||||
DPUP.1 0d05 ; The saved DP3, high byte first the way everything is stored.
|
||||
PSHD.0
|
||||
POPA ; The low half comes off the Stack first.
|
||||
POPB
|
||||
STB.1
|
||||
INCD.1
|
||||
STA.1
|
||||
RETI
|
||||
|
||||
#Data
|
||||
|
||||
QuietText:
|
||||
"quiet: "
|
||||
AnswerText:
|
||||
"answer: "
|
||||
PointerText:
|
||||
"pointer: "
|
||||
Letters:
|
||||
"ABC"
|
||||
|
||||
#Vectors
|
||||
|
||||
Boot start
|
||||
quiet quiet
|
||||
answer answer
|
||||
pointer pointer
|
||||
@@ -0,0 +1,49 @@
|
||||
; A string whose text spells an instruction.
|
||||
;
|
||||
; The quotes are gone by the time the assembler looks at a token, so a string reading "ADD"
|
||||
; used to be assembled as the ADD instruction - which failed with "attempting to assemble
|
||||
; outside the Program Segment", a message about a mistake nobody had made. Mnemonics match
|
||||
; without regard to case, so "or" and "and" were caught by it too, and those are ordinary
|
||||
; enough words to want in a message.
|
||||
;
|
||||
; Correct output is:
|
||||
; ADD OR and NOP
|
||||
|
||||
#Include console.asm
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
SETD.0 First
|
||||
CALL printString
|
||||
CALL blank
|
||||
SETD.0 Second
|
||||
CALL printString
|
||||
CALL blank
|
||||
SETD.0 Third
|
||||
CALL printString
|
||||
CALL blank
|
||||
SETD.0 Fourth
|
||||
CALL printString
|
||||
CALL newLine
|
||||
HALT
|
||||
|
||||
blank:
|
||||
INIA 0x20
|
||||
OUTA 0x00
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
First:
|
||||
"ADD"
|
||||
Second:
|
||||
"OR"
|
||||
Third:
|
||||
"and"
|
||||
Fourth:
|
||||
"NOP"
|
||||
|
||||
#Vectors
|
||||
|
||||
Boot start
|
||||
@@ -14,6 +14,8 @@ SplitBit is a custom 8 bit system designed for hobbyist projects and experimenta
|
||||
- Devices: A bus registry that says what a machine is made of, so a program can ask rather than being told.
|
||||
- Filesystem: SBFS, read and written by SplitBit itself, and by a host tool that speaks the same format so an image can be moved either way.
|
||||
- Loadable Programs: A program that was not booted from carries a header saying where it belongs, and Programs/loader.asm reads one off a disk, puts it there, and runs it.
|
||||
- An Operating System: CosmOS boots the machine, mounts a disk, lists what is on it, loads a program and runs it, and takes the machine back when it finishes. It comes with a library of programs to run, including a game and a line editor that writes files a person typed.
|
||||
- System Services: A loaded program reaches the console and the disk through numbered software interrupts rather than carrying a copy of the code that drives them. The numbers are written down in one file that both sides include, so neither ever types one. It took the editor from 4941 bytes to 1983 without changing a line of what it does.
|
||||
- Storage: A block device with 256 byte blocks and 16 megabytes of them, backed by an image file on the host. It knows blocks and not files, because a filesystem is meant to be software SplitBit runs.
|
||||
- Memory Controller: Reads and writes Program Memory, moves blocks between memory banks, reaches memory that devices bring with them, and guards a range against being written by accident. It is how a SplitBit machine loads a program.
|
||||
- Assembler: Assemble human readable assembly language files directly into SplitBit compatible binary files. Supports including external files, handling labels, alignment and reservation, and defining Program, Data and Vector segments.
|
||||
@@ -66,7 +68,7 @@ The sources are ISO C, and build clean under -std=c11 -pedantic with -Wall -Wext
|
||||
- delete \<image\> \<name\>: Remove one.
|
||||
|
||||
#### Notes:
|
||||
- SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. Until SplitBit can write its own filesystem this is the only way to get a program onto a disk.
|
||||
- SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. SplitBit writes its own filesystem now, so this is not the only way to get something onto a disk; it is still the only way to get a program onto one, since nothing running on the machine assembles anything yet.
|
||||
- Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens put says so rather than putting part of a file on.
|
||||
|
||||
### Usage:
|
||||
@@ -110,6 +112,10 @@ Disk images that tests read from are built by Tests/makedisks.sh before the run,
|
||||
|
||||
The disk tool is checked separately by Tests/disk.sh, which make test runs afterwards: it puts files of every awkward size onto an image and takes them off again, and checks that the things the format says cannot happen are refused.
|
||||
|
||||
Tests/terminal.sh checks the things a recorded output cannot see. Every other test pipes input in and output to a file, which answers what a program prints and is blind to two whole classes of behaviour: **when** something is printed, since piped output is buffered and flushed at exit, so a prompt shown before its answer is asked for and one shown an hour late produce identical files; and **what happens to the terminal**, since key mode only touches one when there is one. Both have gone wrong here, and both were found by a person whose terminal stopped working rather than by anything in this suite. So it runs the emulator under a pseudo-terminal and asks the questions directly: that a prompt arrives before input is read, that a keystroke arrives without Return, that the terminal is handed back however the machine dies, and that suspending and resuming leave it as they found it.
|
||||
|
||||
A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out of it before anything is compared, keeping only whether the program stopped on its own or ran into its limit, which is behaviour. Two instructions added to CosmOS used to move that number in six unrelated files at once, so a real difference would arrive in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own.
|
||||
|
||||
Tests/docs.sh then checks the manuals against the code: that every instruction has a row and every row is an instruction, that the counts in the headings are right, that every directive is written down, that every routine the manuals promise exists, and that the worked examples still assemble to the bytes printed beside them. Documentation goes stale quietly, and this is what stops it.
|
||||
|
||||
Tests are defined in Tests/manifest, one line per program. To record the current output as the expected result, after you have checked that it is correct:
|
||||
|
||||
@@ -166,8 +166,18 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
||||
exit(1);
|
||||
}
|
||||
// Read off tokens.
|
||||
while (readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
||||
if ((size_t)*intermediateIndex >= *arraySize - 1) {
|
||||
//
|
||||
// ROOM IS MADE BEFORE THE TOKEN IS READ, not after. readToken writes into the element
|
||||
// at the current index, so a check that came afterwards was checking whether the write
|
||||
// that had already happened was allowed to. It survived for a long time because the
|
||||
// margin usually covered it, and stopped surviving when a file grew past a doubling:
|
||||
// several paths below take a SECOND element for one token - an #Include takes one for
|
||||
// the file name, #Align and #Reserve take one for the count - so the index can move by
|
||||
// two in an iteration and step straight over a margin of one.
|
||||
//
|
||||
// The margin is two for that reason, which is the most any one iteration uses.
|
||||
while (1) {
|
||||
if ((size_t)*intermediateIndex + 2 >= *arraySize) {
|
||||
size_t grownSize = *arraySize * 2; // Double the size of the array.
|
||||
// Into a temporary, so that the old allocation is still ours to free if
|
||||
// this fails, rather than being lost the moment realloc returns NULL.
|
||||
@@ -184,7 +194,9 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
||||
*intermediateArray = grown;
|
||||
*arraySize = grownSize;
|
||||
}
|
||||
//printf("Token number %d\n", intermediateIndex);
|
||||
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
||||
break;
|
||||
}
|
||||
// Go ahead and mark what we already know about this token.
|
||||
(*intermediateArray)[*intermediateIndex].fileName = fileName;
|
||||
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
|
||||
@@ -311,8 +323,14 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
|
||||
status = VECTORS;
|
||||
break;
|
||||
}
|
||||
// Next, check to see if it's an instruction.
|
||||
} else if (checkIfInstruction(&(*intermediateArray)[*intermediateIndex])) {
|
||||
// Next, check to see if it's an instruction. A string is never one, however it is
|
||||
// spelled: the quotes are gone by the time anything looks at a token, so a string
|
||||
// whose text happens to be a mnemonic looked exactly like that instruction and was
|
||||
// assembled as one. Mnemonics are matched without regard to case, so this was not
|
||||
// only a problem for a program with "ADD" in its data - "or" and "and" are ordinary
|
||||
// enough words to find in a message.
|
||||
} else if ((*intermediateArray)[*intermediateIndex].type != STRING
|
||||
&& checkIfInstruction(&(*intermediateArray)[*intermediateIndex])) {
|
||||
// We should check if we're set up to mark this for the Program Segment.
|
||||
if (status != PROGRAM) {
|
||||
fprintf(stderr, RED "Error: Attempting to assemble outside the Program Segment.\n Did you forget to use the #Program keyword?\n" RESET);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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 How many vectors follow the data, zero in a version 1 file
|
||||
// 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, then the vectors
|
||||
//
|
||||
// ---- Vectors, added in version two ----
|
||||
//
|
||||
// Four bytes each, the same shape a boot image uses: the address of the vector table slot,
|
||||
// then the address to put in it. Both most significant byte first. Saying the slot outright
|
||||
// rather than the vector number means the loader does no arithmetic and does not need to
|
||||
// know where either vector table begins, and one entry can name a software or a hardware
|
||||
// vector without saying which.
|
||||
//
|
||||
// A PROGRAM CARRYING VECTORS SAYS VERSION TWO, and one carrying none stays version one and
|
||||
// loads anywhere. The version is not decided by the format's age but by whether the file
|
||||
// needs something of its loader: an older loader meeting a version two file refuses it and
|
||||
// says so, which is the right answer, because a program whose handlers were quietly dropped
|
||||
// is not the program somebody asked for. It would run, and then fail later at a place with
|
||||
// nothing to connect it back to loading.
|
||||
//
|
||||
// INSTALLING THEM IS THE LOADER'S JOB, AND SO IS TAKING THEM BACK. A vector points into the
|
||||
// program that supplied it, so leaving one installed after that program is gone aims an
|
||||
// interrupt at whatever occupies those addresses next. A loader restores what it found.
|
||||
#define SBEX_VECTOR_ENTRY_BYTES 4
|
||||
//
|
||||
// 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
|
||||
// What a program says when it asks nothing of its loader beyond code and data, and what
|
||||
// it says when it also brings vectors that have to be installed.
|
||||
#define SBEX_VERSION 1
|
||||
#define SBEX_VERSION_VECTORS 2
|
||||
#define SBEX_HEADER_BYTES 16
|
||||
|
||||
#define SBEX_VERSION_AT 4
|
||||
#define SBEX_VECTORS_AT 5
|
||||
#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
|
||||
@@ -57,7 +57,10 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
|
||||
if (debugSecondPass) printf("Added label %s with address %04X\n", labelName, labelArray[labelCount].address);
|
||||
labelCount++;
|
||||
} else {
|
||||
fprintf(stderr, "Error: Too many labels defined.\n");
|
||||
fprintf(stderr, RED "Error: Too many labels. This program and everything it\n"
|
||||
" includes may define %d between them, and \"%s\" is one too many.\n" RESET,
|
||||
MAX_LABELS, labelName);
|
||||
printf("File: %s at line %d.\n", fileName, lineNumber);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -564,16 +567,43 @@ static void writeLoadable(const char *outputFileName, uint8_t *Program, int prog
|
||||
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, outputFileName);
|
||||
exit(1);
|
||||
}
|
||||
// Boot says where a program begins, which is what the entry field holds, so in a
|
||||
// loadable program that line fills it in. It is NOT installed as vector 0: that entry
|
||||
// is where the whole machine starts, and a program being loaded into a running system
|
||||
// has no business saying anything about that.
|
||||
//
|
||||
// Without a Boot line the entry is the first byte of the code, which is where a
|
||||
// program begins if it does not say otherwise.
|
||||
uint16_t entry = codeBase;
|
||||
int installed = 0;
|
||||
for (int i = 0; i < vectorArrayCount; i++) {
|
||||
if (vectorArray[i].declaredOnly) {
|
||||
continue;
|
||||
}
|
||||
if (vectorArray[i].base == SOFTWARE_VECTOR_BASE
|
||||
&& vectorArray[i].index == VECTOR_BOOT) {
|
||||
entry = vectorArray[i].handler;
|
||||
continue;
|
||||
}
|
||||
installed++;
|
||||
}
|
||||
if (installed > 255) {
|
||||
fprintf(stderr, RED "Error: A loadable program may bring at most 255 vectors.\n" RESET);
|
||||
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;
|
||||
// A program that brings vectors needs something of its loader that a version one
|
||||
// loader does not know how to give, so it says so, and an older one refuses it rather
|
||||
// than running it without them.
|
||||
header[SBEX_VERSION_AT] = installed > 0 ? SBEX_VERSION_VECTORS : SBEX_VERSION;
|
||||
header[SBEX_VECTORS_AT] = (uint8_t)installed;
|
||||
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_ENTRY_AT] = (uint8_t)(entry >> 8);
|
||||
header[SBEX_ENTRY_AT + 1] = (uint8_t)(entry & 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);
|
||||
@@ -583,10 +613,36 @@ static void writeLoadable(const char *outputFileName, uint8_t *Program, int prog
|
||||
fwrite(header, 1, sizeof(header), outputFile);
|
||||
fwrite(Program + codeBase, 1, (size_t)codeLength, outputFile);
|
||||
fwrite(Data + dataBase, 1, (size_t)dataLength, outputFile);
|
||||
|
||||
// The vectors, last, so that everything before them sits where a version one loader
|
||||
// already expects to find it.
|
||||
for (int i = 0; i < vectorArrayCount; i++) {
|
||||
if (vectorArray[i].declaredOnly) {
|
||||
continue;
|
||||
}
|
||||
if (vectorArray[i].base == SOFTWARE_VECTOR_BASE
|
||||
&& vectorArray[i].index == VECTOR_BOOT) {
|
||||
continue;
|
||||
}
|
||||
uint16_t slot = vectorArray[i].base + (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
|
||||
fputc((slot >> 8) & 0xFF, outputFile);
|
||||
fputc(slot & 0xFF, outputFile);
|
||||
fputc((vectorArray[i].handler >> 8) & 0xFF, outputFile);
|
||||
fputc(vectorArray[i].handler & 0xFF, 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);
|
||||
printf(GREEN " Code: %d bytes at 0x%04X.\n Data: %d bytes at 0x%04X.\n" RESET,
|
||||
codeLength, codeBase, dataLength, dataBase);
|
||||
if (entry != codeBase) {
|
||||
printf(GREEN " Starts at 0x%04X.\n" RESET, entry);
|
||||
}
|
||||
if (installed > 0) {
|
||||
printf(GREEN " Vectors: %d. Version 2, so a loader that cannot install them will say so.\n" RESET,
|
||||
installed);
|
||||
}
|
||||
printf(GREEN " Total size: %d bytes.\n" RESET,
|
||||
SBEX_HEADER_BYTES + codeLength + dataLength + installed * SBEX_VECTOR_ENTRY_BYTES);
|
||||
}
|
||||
|
||||
void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCount, uint8_t *Data, int dataCount) {
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
#include <ctype.h>
|
||||
#include "Assm-util.h"
|
||||
|
||||
#define MAX_LABELS 256
|
||||
// Every label in a program and in everything it includes shares one table, because they
|
||||
// share one namespace: a name may only be defined once across the whole assembly. So this
|
||||
// is not the size of one file but the size of a program and its libraries together, and
|
||||
// CosmOS with its four libraries went past 256 while still being a small system.
|
||||
#define MAX_LABELS 1024
|
||||
#define MAX_VECTORS 256
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -146,7 +146,41 @@ 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.
|
||||
A program starts at its code base unless it says otherwise, and `Boot` in its Vector Segment is how it says otherwise:
|
||||
|
||||
```
|
||||
#Program
|
||||
#Base 0x2000
|
||||
helpers:
|
||||
...
|
||||
start:
|
||||
...
|
||||
|
||||
#Vectors
|
||||
Boot start ; Which is where this program begins.
|
||||
```
|
||||
|
||||
That fills in the entry point in the header. It is not installed as vector 0 of the machine, which is where everything begins at power on and no business of a program being loaded into a system that is already running.
|
||||
|
||||
### Vectors In A Loadable Program:
|
||||
|
||||
Everything else in a Vector Segment is carried in the file and installed by whatever loads the program. That is what lets a loaded program be interrupted: a handler is an address in the vector table, and until the format could carry one, a program that was not the one the machine booted from had no way to ask for it.
|
||||
|
||||
```
|
||||
#Vectors
|
||||
Boot start
|
||||
Device 0x00 keyHandler ; The console, which now interrupts this program.
|
||||
```
|
||||
|
||||
A program carrying vectors is written out as version two of the format, and the assembler says so:
|
||||
|
||||
```
|
||||
Vectors: 1. Version 2, so a loader that cannot install them will say so.
|
||||
```
|
||||
|
||||
A program carrying none stays version one and loads anywhere. The difference matters because a loader that does not understand version two refuses the file rather than running a program with its handlers missing, which would work until the moment it was supposed to be interrupted and then fail somewhere with nothing pointing back at the cause.
|
||||
|
||||
Whoever loads the program is expected to take the vectors out again when it finishes. See the Programming Manual.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -454,9 +454,9 @@ Status bit 1 says the last operation failed: there is no disk, or the block aske
|
||||
|
||||
A disk error is not a fault. Faults on this machine mean it cannot continue, and a read that fails is an ordinary thing that happens to working programs on failing media. It is reported so that a program can cope with it, rather than stopping the machine and taking the choice away.
|
||||
|
||||
## Reading The Filesystem:
|
||||
## Reading And Writing The Filesystem:
|
||||
|
||||
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm reads one.
|
||||
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one.
|
||||
|
||||
| Routine | Does |
|
||||
| --- | --- |
|
||||
@@ -467,11 +467,35 @@ The disk knows blocks and nothing else, so a filesystem is software. Programs/Co
|
||||
| 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. |
|
||||
| sbfsDelete | DP0 names a file. Frees its entry and its blocks. Q is zero if it went. |
|
||||
| sbfsRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it was renamed. Refused if something already answers to the new name. |
|
||||
| sbfsSaveFile | DP0 names the file, DP1 is the data, and SbfsFileBlocks with SbfsFileTail say how big it now is. Writes it whether or not it was there before, and whatever size it used to be. |
|
||||
|
||||
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.
|
||||
|
||||
### Saving Something Twice:
|
||||
|
||||
Which is why saving a document is not the same as writing a file, and why sbfsSaveFile exists rather than each tool doing it. A file that has grown will usually not fit where it was, so saving it means putting it somewhere else and letting go of where it was — and **the obvious order is a trap**:
|
||||
|
||||
```
|
||||
delete the old one
|
||||
make a new one <- refused, and the old one is already gone
|
||||
write it
|
||||
```
|
||||
|
||||
A create can be refused for want of a run long enough even on a disk with plenty of free blocks, because free blocks are only useful to a contiguous file when they are next to each other. Done in that order, the first fragmented disk somebody meets eats their work. sbfsSaveFile does it the other way round:
|
||||
|
||||
```
|
||||
make a temporary nothing is lost if there is nowhere to put it
|
||||
write it
|
||||
delete the original only now, once the new one is safely down
|
||||
rename the temporary
|
||||
```
|
||||
|
||||
**That is what renaming is for.** It looks like a convenience and it is the safety mechanism: it is the only one of the three operations that moves no data — a name lives in the directory entry, so renaming writes twenty two bytes into one block — which makes it the only one that can be left until last and relied on not to fail.
|
||||
|
||||
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.
|
||||
|
||||
A file's length is its block count times 256 plus its tail, which is the same as putting the block count in the high byte and the tail in the low one. Nothing pads a file out, so the bytes after the end of one are whatever else happened to be in that block, and it is the reading program's business to stop where the tail says.
|
||||
@@ -519,6 +543,68 @@ Every routine names the Data Pointer it works through rather than assuming there
|
||||
|
||||
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.
|
||||
|
||||
## What A Program May Ask The System For:
|
||||
|
||||
A loaded program is on its own hardware and can do anything the machine can do — it is a fence, not a wall. But the things it usually wants are things the system is already doing, and asking is both shorter and the only way to reach code that was assembled separately. `CALL` needs a label, and a label has to be in the same assembly; `SWI` needs only a number both sides agree on.
|
||||
|
||||
Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, which both the system and the program include. Neither side ever types a number.
|
||||
|
||||
| Service | Does |
|
||||
| --- | --- |
|
||||
| osPrintString | DP0 names a string ending in a zero byte. Prints it. |
|
||||
| osReadLine | DP0 names somewhere to put a line, B says how much room there is. Reads one from the console. Q comes back holding how long it was. |
|
||||
| osExit | Gives the machine back. Does not return. |
|
||||
| osArgument | DP0 names somewhere to put whatever followed the run command, B says how much room there is. |
|
||||
| osFileRead | DP0 names a file, DP1 says where to put it. Q is zero if it read, and DP3 comes back holding how many bytes there were. |
|
||||
| osFileSave | DP0 names a file, DP1 is the bytes, A and B together are how many. Q is zero if it saved, whether or not it was there before. |
|
||||
| osFileDelete | DP0 names a file. Q is zero if it went. |
|
||||
| osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. |
|
||||
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
|
||||
|
||||
```
|
||||
#Include services.asm
|
||||
...
|
||||
SETD.0 Message
|
||||
SWI osPrintString
|
||||
```
|
||||
|
||||
### The Disk Without A Filesystem:
|
||||
|
||||
A program that wants a file does not need to know what a filesystem is. Before these existed it had to include the whole of `sbfs.asm` — two and a half kilobytes of a private copy of code the system already had running — and then mount a disk that was already mounted.
|
||||
|
||||
There is no service to mount one, and that is not an omission. The system mounts the disk before it reads its first prompt, and there is one disk with one buffer registered as one bank; a program mounting it again was only ever an artefact of owning a second copy of the library. That call disappears rather than moving.
|
||||
|
||||
Sizes fit the registers exactly, in both directions. A file that can be read into Data Memory is under 64K by definition, so its length is sixteen bits: coming back it is DP3, and going out it is A and B together. Neither direction needs a record in memory whose shape both sides have to agree on.
|
||||
|
||||
A file of 256 blocks or more is refused by `osFileRead` rather than partly read, because 64K will not fit in Data Memory and its length will not fit in the pointer that reports it. A length that lies would be worse than a file that will not open.
|
||||
|
||||
`Programs/CosmOS/Apps/Files.asm` does the whole round trip — write, read, report, rename, delete — in 645 bytes, and includes nothing but the service names.
|
||||
|
||||
`osArgument` is how a program is told what it is for. Everything written before it did the same thing however it was started, which is fine for a program that greets you and no use to one that edits a named document. What arrives is the whole rest of the line, spaces and all, rather than a list of words: what counts as an argument is the program's business, and handing over what was typed is the system's.
|
||||
|
||||
A handler is entered with the caller's registers exactly as they were, because an interrupt frame is pushed rather than cleared. That is why a service can be given a pointer in DP0 and a count in B without any of it being copied anywhere first.
|
||||
|
||||
### How A Service Answers:
|
||||
|
||||
The same thing that makes an interrupt safe makes a service mute. RETI restores every register from the frame, so whatever a handler worked out is thrown away on the way out — which is exactly right for a device interrupting at a moment nobody chose, and useless for a service that was asked a question.
|
||||
|
||||
A service answers by **writing into its own frame**, over the saved register, and letting RETI put it back. MVSD copies the Stack Pointer into a Data Pointer and the frame sits just above it, so returning a byte in Q is three instructions:
|
||||
|
||||
```
|
||||
answer:
|
||||
INIA 0d42
|
||||
MVSD.1
|
||||
DPUP.1 0d02 ; The saved Q. See the frame table under Interrupts.
|
||||
STA.1
|
||||
RETI
|
||||
```
|
||||
|
||||
**Which registers a service may answer in is the convention CALL already has: Q and DP3.** A subroutine cannot hand back A, B or Data Pointers 0 to 2 because RET puts them back; a service *could* write over any of them and should not, for exactly the reason that list exists. A caller is entitled to find what it kept still there.
|
||||
|
||||
**Only the handler itself can do this.** The offsets are from wherever the Stack Pointer is, and a CALL moves it by ten — so a routine called by a handler that tried the same thing would be writing into its own return address. The poke belongs inline, next to the RETI.
|
||||
|
||||
A service that has nothing to say does nothing, and the caller's registers arrive back untouched. That is worth knowing from the other side too: a service cannot corrupt a register by accident, only by deciding to.
|
||||
|
||||
## 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.
|
||||
@@ -526,19 +612,31 @@ A program that was not the one the machine booted from carries sixteen bytes in
|
||||
| Offset | Size | Holds |
|
||||
| --- | --- | --- |
|
||||
| 0 | 4 | SBEX |
|
||||
| 4 | 1 | Version. One. |
|
||||
| 5 | 1 | Reserved. |
|
||||
| 4 | 1 | Version. One, or two if it brings vectors. |
|
||||
| 5 | 1 | How many vectors follow the data. Zero in a version one file. |
|
||||
| 6 | 2 | Where the code goes in Program Memory. |
|
||||
| 8 | 2 | Where to start running. |
|
||||
| 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, and then the data. |
|
||||
| 16 | | The code, then the data, then the vectors. |
|
||||
|
||||
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.
|
||||
|
||||
### Bringing Vectors:
|
||||
|
||||
A program that only wants to be run needs nothing here and says version one. A program that wants a handler installed needs something of whoever loads it, and says version two.
|
||||
|
||||
Each vector is four bytes: the address of the slot in the vector table, then the address to put in it, both most significant byte first. Naming the slot rather than the vector number means the loader does no arithmetic and does not have to know where either vector table begins, and one entry can be a software or a hardware vector without saying which it is.
|
||||
|
||||
**A version two file is refused by a loader that cannot install them.** That is the point of the version rather than an inconvenience of it. A program whose handlers were quietly dropped would load, run, and then go wrong somewhere with nothing to connect the failure back to loading — a game waiting for keys that no longer arrive. Failing once, at load, with a reason, is worth more than running.
|
||||
|
||||
**Whoever installs them takes them back.** A vector points into the program that supplied it, so one left in the table after that program has gone aims an interrupt at whatever occupies those addresses next. CosmOS keeps its own copy of what a program brought, puts them in when the program is run and not when it is loaded, and restores what was underneath them when the program gives the machine back. Restoring, rather than clearing: a program is allowed to install a handler over one the system was already using, and when it goes, what it covered up has to come back rather than become a hole.
|
||||
|
||||
`Boot` in a loadable program fills in the entry field, since that is what it means, and is not installed as vector 0 — where the machine starts is not a loaded program's business. Without one, a program begins at the first byte of its code.
|
||||
|
||||
### Where A Program Says It Lives:
|
||||
|
||||
**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.
|
||||
@@ -559,6 +657,93 @@ That is not general placement: a base applies to a whole segment, and only the f
|
||||
|
||||
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.
|
||||
|
||||
## Programs That Come With The System:
|
||||
|
||||
`Programs/CosmOS/Apps` holds what the shell can load. Several are old programs written for the bare machine that needed five edits each to become loadable ones — the Fibonacci and sieve programs, `greet`, and `hello`. The rest were written for the system as it is now, and each of those exists to show one thing working:
|
||||
|
||||
| Program | What it is for |
|
||||
| --- | --- |
|
||||
| Life | Conway's Game of Life, which had to be taught to stop, since a program that never ends takes the shell with it. Polls the console between generations. |
|
||||
| Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. |
|
||||
| Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. |
|
||||
| Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. |
|
||||
| Files | Writes a file, reads it back, renames it and deletes it, in 645 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
|
||||
| Edit | A line editor. |
|
||||
|
||||
### The Monitor:
|
||||
|
||||
The monitor is **part of the shell**, not a program the shell loads, and that is the whole reason it works. A loaded program occupies the one place a loaded program goes, so a monitor that was an application could never look at any other application: loading the thing you wanted to inspect would replace the thing doing the inspecting.
|
||||
|
||||
`monitor` turns it on and the prompt changes from `>` to `*`. It is **a mode, not a detour** — the shell's own commands still work, and the mode persists until you say otherwise:
|
||||
|
||||
```
|
||||
> load Snake.sbx
|
||||
> monitor
|
||||
* d 2000
|
||||
2000 47 00 11 00 SETD.0 1100
|
||||
* b data
|
||||
bank 01
|
||||
* x 1000
|
||||
* exit
|
||||
>
|
||||
```
|
||||
|
||||
**A program giving the machine back lands at the prompt it was started from**, so `g` into something, letting it run, and having it exit puts you back at `*` rather than at the shell. That falls out of the mode being a variable the prompt reads rather than a second loop: every way back to the prompt goes through one place, including `osExit`. Looking at a program and running it therefore do not interrupt each other, which is the thing a monitor is for.
|
||||
|
||||
`exit` leaves whatever you are in — the monitor if you are in it, the machine if you are not.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `x [addr]` | Sixty-four bytes, as hex and as characters |
|
||||
| `d [addr]` | Eight instructions, disassembled |
|
||||
| `s addr b b …` | Put those bytes there |
|
||||
| `b program\|data\|n` | Which bank to look at |
|
||||
| `g addr` | Go there |
|
||||
|
||||
`x` and `d` share one cursor and each leaves it past what it showed, so without an address either carries on — reading through memory is one letter at a time, and you can switch between bytes and instructions without retyping where you are. `s` deliberately does not move it.
|
||||
|
||||
Everything else here does something; the monitor looks at what the others did. It shows memory as hex and as characters, disassembles it, writes bytes into it, and jumps to an address — all through the memory controller, which is the only thing that can reach Program Memory.
|
||||
|
||||
That is why a monitor is worth more on this machine than on most. Data Memory a program can already read for itself with a Data Pointer. The half it cannot see is Program Memory, and that is the half its bugs are in.
|
||||
|
||||
**Its instruction table is generated from the assembler's**, by `Tests/instructiontable.py`, and checked against it by `Tests/docs.sh` — along with a second check that the lengths that table implies are the ones the manual's own Bytes column prints. Both matter for the same reason: a disassembler that disagreed about how long an instruction is would not print one line wrong, it would lose its place and print everything after it wrong. Which is what a disassembler does anyway when it starts in the middle of an instruction, and is worth seeing once so it is recognised later.
|
||||
|
||||
**Where to put something you typed in yourself** is a question the monitor answers, because the answer moves every time the monitor is rebuilt. `m` says where its own two segments end, and those are the first free addresses:
|
||||
|
||||
```
|
||||
> m
|
||||
code from 2000, free from 2607
|
||||
data from 1000, free from 1367 up to the stack
|
||||
```
|
||||
|
||||
Which is what makes the monitor's real trick possible — a program that no assembler ever saw:
|
||||
|
||||
```
|
||||
> s 8000 26 48 D1 00 26 49 D1 00 26 0A D1 00 18 12
|
||||
> d 8000
|
||||
8000 26 48 INIA 48
|
||||
8002 D1 00 OUTA 00
|
||||
8004 26 49 INIA 49
|
||||
...
|
||||
800C 18 12 SWI 12
|
||||
> g 8000
|
||||
HI
|
||||
```
|
||||
|
||||
Typed in as bytes, checked by disassembling it back, and run. It ends with `SWI osExit`, which is how it gives the machine to the shell rather than to nothing.
|
||||
|
||||
There are no breakpoints yet, and `g` does not come back. The machinery for both already exists and nothing has used it: SplitBit has 192 undecodable bytes, and an invalid opcode dispatches through the `BadOpcode` vector carrying **the address of the offending byte**. A breakpoint is a spare byte written over an instruction and a handler waiting for it.
|
||||
|
||||
### The Editor:
|
||||
|
||||
`Edit` is the first program on this machine that makes a file a person typed — every byte on every disk before it was put there by the host tool. It is line oriented in the manner of `ed`: `l` lists, `a` adds at the end, `i` and `c` and `d` take a line number, `w` writes and `q` stops.
|
||||
|
||||
It includes nothing but `services.asm` and `text.asm`: the filesystem and the console are the system's, asked for rather than carried. That is what took it from 4,941 bytes to 1,983 without a line of its own logic changing — and the way that was checked is worth knowing, because the recorded output of the `cosmosEdit` test did not move by a single byte across the rewrite.
|
||||
|
||||
It keeps the document as a **linked list of lines** rather than one buffer with newlines in it. Each line says where the next one is, how long it is, and then its bytes. Inserting is two pointers changed and nothing moved; with a flat buffer it would mean shifting every byte after the edit, on a machine whose only block move is a device asked politely. The price is that deleted lines are not reused, so a heavy session uses more room than the document needs and writing it out is what tidies up.
|
||||
|
||||
Saving goes through `sbfsSaveFile`, so a document that has grown is written somewhere else and the original is only let go of once the new one is safely down. That is the whole reason the editor was written: not because the machine needed an editor, but because every tool that produces a file needs the same four operations, and building them for one imaginary tool is how they end up wrong.
|
||||
|
||||
## Refusing:
|
||||
|
||||
A device can refuse what it was asked to do. This is not the same as interrupting. An interrupt is a device asking for attention later, answered between instructions once the CPU is ready. A refusal is a device saying no to the instruction happening now, so the machine stops where it stands rather than carrying on as though the access had worked.
|
||||
|
||||
+128
-1
@@ -131,6 +131,35 @@ else:
|
||||
problems.append("the Assembler Manual does not say the automatic vectors start"
|
||||
" at %d" % autoFrom)
|
||||
|
||||
# ---- The loadable header table matches the offsets the assembler writes ----
|
||||
#
|
||||
# The Programming Manual prints the header field by field, which is the description two
|
||||
# implementations work from. sbex.h is where the offsets actually are, so a field moved
|
||||
# there and not here would leave the manual describing a format nobody writes.
|
||||
sbex = read("Source/Assembler/sbex.h")
|
||||
offsets = {name: int(value)
|
||||
for name, value in re.findall(r'^#define (SBEX_[A-Z_]+_AT)\s+(\d+)$', sbex, re.M)}
|
||||
if "## Loading A Program From A Disk:" not in pm:
|
||||
problems.append("the Programming Manual has lost its loadable program section")
|
||||
else:
|
||||
loading = pm.split("## Loading A Program From A Disk:")[1].split("\n## ")[0]
|
||||
listed = [int(m) for m in re.findall(r'^\| (\d+) \| \d* \|', loading, re.M)]
|
||||
for name, offset in sorted(offsets.items(), key=lambda pair: pair[1]):
|
||||
if offset not in listed:
|
||||
problems.append("%s is at offset %d and the header table has no row for it"
|
||||
% (name, offset))
|
||||
# Spelled as a word, the way these manuals write small numbers in prose.
|
||||
asWord = {1: "one", 2: "two", 3: "three", 4: "four", 5: "five"}
|
||||
for version in ("SBEX_VERSION", "SBEX_VERSION_VECTORS"):
|
||||
number = re.search(r'^#define %s\s+(\d+)$' % version, sbex, re.M)
|
||||
if not number:
|
||||
problems.append("%s is gone from sbex.h" % version)
|
||||
continue
|
||||
said = asWord.get(int(number.group(1)))
|
||||
if said is None or said not in loading.lower():
|
||||
problems.append("the loadable program section does not mention version %s (%s)"
|
||||
% (number.group(1), said))
|
||||
|
||||
# ---- Every console status bit is described ----
|
||||
#
|
||||
# The status port is read by writing a mask and testing it, so a program can only use a bit
|
||||
@@ -150,6 +179,104 @@ else:
|
||||
problems.append("%s is bit %d of the console status port and The Console does"
|
||||
" not mention it" % (name, bit))
|
||||
|
||||
# ---- Every service the system offers has a row ----
|
||||
#
|
||||
# services.asm is the one place the numbers are written, and both the system and every
|
||||
# program include it. A service added there and not here is one nothing can find out about
|
||||
# except by reading the source of the operating system.
|
||||
# DECLARING A SERVICE AND IMPLEMENTING ONE ARE DIFFERENT THINGS, and the manual should
|
||||
# describe the second. services.asm names them and fixes their numbers, which is what lets a
|
||||
# number be pinned before anything answers to it; cosmos.asm is where a name gets a handler.
|
||||
# A row for a service nothing implements would be describing a call that faults, and a
|
||||
# missing row for one that works is a service nobody can find out about.
|
||||
services = read("Programs/CosmOS/Source/services.asm")
|
||||
named = set(re.findall(r'^\s{2}(os[A-Za-z]+)\s+0d\d+', services, re.M))
|
||||
system = read("Programs/CosmOS/Source/cosmos.asm")
|
||||
vectors = system.split("#Vectors")[-1] if "#Vectors" in system else ""
|
||||
implemented = {name for name in re.findall(r'^\s{2}(os[A-Za-z]+)\s+[a-zA-Z]', vectors, re.M)
|
||||
if name in named}
|
||||
if not named:
|
||||
problems.append("no services could be found in services.asm")
|
||||
elif "## What A Program May Ask The System For:" not in pm:
|
||||
problems.append("the Programming Manual has lost its services section")
|
||||
else:
|
||||
section = pm.split("## What A Program May Ask The System For:")[1].split("\n## ")[0]
|
||||
documented = set(re.findall(r'^\| (os[A-Za-z]+) \|', section, re.M))
|
||||
for name in sorted(implemented - documented):
|
||||
problems.append("%s is a service the system implements and has no row in the"
|
||||
" services table" % name)
|
||||
for name in sorted(documented - implemented):
|
||||
problems.append("the services table describes %s, which nothing implements: calling"
|
||||
" it would dispatch through an empty vector and fault" % name)
|
||||
|
||||
# ---- Every program the manual describes is really there ----
|
||||
#
|
||||
# The table names what the shell can load. A program renamed or removed leaves a row
|
||||
# describing something nobody can run, which is the same kind of quiet wrongness as a
|
||||
# routine that no longer exists. The other direction is deliberately not checked: the ported
|
||||
# programs are covered in the prose rather than given a row each.
|
||||
import os
|
||||
if "## Programs That Come With The System:" not in pm:
|
||||
problems.append("the Programming Manual has lost its list of programs")
|
||||
else:
|
||||
listed = pm.split("## Programs That Come With The System:")[1].split("\n### ")[0]
|
||||
# After the separator, so the table's own heading row is not mistaken for a program.
|
||||
listed = listed.split("| --- |")[-1]
|
||||
for name in re.findall(r'^\| ([A-Z][A-Za-z0-9-]*) \|', listed, re.M):
|
||||
if not os.path.exists("Programs/CosmOS/Apps/%s.asm" % name):
|
||||
problems.append("the manual describes a program called %s, and there is no"
|
||||
" Programs/CosmOS/Apps/%s.asm" % (name, name))
|
||||
|
||||
# ---- The monitor's instruction table is the assembler's ----
|
||||
#
|
||||
# The monitor disassembles, so it needs the same 64 instructions with the same names and the
|
||||
# same lengths. A disassembler that disagreed about a length would not print one line wrong,
|
||||
# it would lose its place and print everything after it wrong, which is the worst way for a
|
||||
# tool like that to fail: confidently. So the table is generated from assembly.c by
|
||||
# Tests/instructiontable.py, and what is in the monitor is checked against it here.
|
||||
import subprocess
|
||||
generated = subprocess.run([sys.executable, "Tests/instructiontable.py"],
|
||||
capture_output=True, text=True)
|
||||
if generated.returncode != 0:
|
||||
problems.append("the instruction table generator would not run")
|
||||
else:
|
||||
wanted = [line.rstrip() for line in generated.stdout.splitlines() if line.strip()]
|
||||
monitor = read("Programs/CosmOS/Source/cosmos.asm")
|
||||
if "\nInstructions:\n" not in monitor:
|
||||
problems.append("the system has lost its instruction table")
|
||||
else:
|
||||
block = monitor.split("\nInstructions:\n")[1]
|
||||
have = []
|
||||
for line in block.splitlines():
|
||||
if not line.strip() or not line.startswith(" 0x"):
|
||||
break
|
||||
have.append(line.rstrip())
|
||||
if have != wanted:
|
||||
problems.append("the system's instruction table is not what the assembler's"
|
||||
" instruction set generates: %d entries against %d, first"
|
||||
" difference at %s"
|
||||
% (len(have), len(wanted),
|
||||
next((a or b for a, b in zip(have + [None] * len(wanted),
|
||||
wanted + [None] * len(have))
|
||||
if a != b), "the end")))
|
||||
|
||||
# ---- And the lengths that table implies are the ones the manual prints ----
|
||||
#
|
||||
# The generator works out how long each instruction is from rules written in it; the manual
|
||||
# says so in a column somebody typed. They are independent accounts of the same fact, which
|
||||
# is exactly the pair worth checking against each other.
|
||||
sys.path.insert(0, "Tests")
|
||||
import instructiontable
|
||||
lengthOf = {0: 1, 1: 3, 2: 2, 3: 2, 4: 3, 5: 4, 6: 3}
|
||||
printed = {}
|
||||
for m in re.finditer(r'^\|\s*[0-9A-F]{2}\s*\|\s*([A-Z][A-Z0-9]*)\s*\|\s*(\d+)\s*\|', pm, re.M):
|
||||
printed[m.group(1)] = int(m.group(2))
|
||||
for opcode, name in instructiontable.table():
|
||||
implied = lengthOf[instructiontable.shapeOf(opcode)]
|
||||
if name in printed and printed[name] != implied:
|
||||
problems.append("the manual says %s is %d bytes and the disassembler will read it"
|
||||
" as %d" % (name, printed[name], implied))
|
||||
|
||||
# ---- 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:
|
||||
@@ -160,7 +287,7 @@ for directive in sorted(set(re.findall(r'"(#[A-Za-z]+)"', util))):
|
||||
# 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"),
|
||||
for heading, library in [("## Reading And Writing 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"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
0000 0001 0001 0002 0003 0005 0008 000D 0015 0022 0037 0059 0090 00E9 0179 0262 03DB 063D 0A18 1055 1A6D 2AC2 452F 6FF1
|
||||
Execution halted after 2534 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -222,5 +222,5 @@
|
||||
|
||||
|
||||
|
||||
Execution stopped after 3000000 cycles. (cycle limit reached)
|
||||
Execution stopped. (cycle limit reached)
|
||||
[exit 0]
|
||||
|
||||
@@ -222,5 +222,5 @@
|
||||
|
||||
|
||||
|
||||
Execution stopped after 3000000 cycles. (cycle limit reached)
|
||||
Execution stopped. (cycle limit reached)
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
00000000 00000001 00000001 00000002 00000003 00000005 00000008 0000000D 00000015 00000022 00000037 00000059 00000090 000000E9 00000179 00000262 000003DB 0000063D 00000A18 00001055 00001A6D 00002AC2 0000452F 00006FF1 0000B520 00012511 0001DA31 0002FF42 0004D973 0007D8B5 000CB228 00148ADD 00213D05 0035C7E2 005704E7 008CCCC9 00E3D1B0 01709E79 02547029 03C50EA2 06197ECB 09DE8D6D 0FF80C38 19D699A5 29CEA5DD 43A53F82 6D73E55F
|
||||
Execution halted after 10610 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
0 1 1 2 3 5 8 13 21 34 55 89 144 233
|
||||
Execution halted after 1506 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251
|
||||
Execution halted after 54061 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Fault: The device on port 233 refused the access at Program Address 0x001D, and nothing is installed to deal with it.
|
||||
O
|
||||
Execution halted after 16 cycles.
|
||||
Execution halted.
|
||||
[exit 1]
|
||||
|
||||
@@ -3,5 +3,5 @@ blitted
|
||||
DE AD
|
||||
untouched
|
||||
ABABCDEFGH
|
||||
Execution halted after 464 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
QAB C qab c
|
||||
9876543210
|
||||
Execution halted after 176 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -3,5 +3,5 @@ asked for: 0D ready keys interrupts
|
||||
what arrived:
|
||||
keys
|
||||
at the end: 02 ended
|
||||
Execution halted after 1597 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -6,5 +6,5 @@ hi
|
||||
|
||||
at the end: 06 ended keys
|
||||
line mode: 02 ended
|
||||
Execution halted after 891 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -10,5 +10,5 @@ lines read:
|
||||
[SplitBit] 8
|
||||
[a line that is f] 16
|
||||
end of input
|
||||
Execution halted after 6064 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
DE AD BE EF
|
||||
01 FF
|
||||
00 00
|
||||
Execution halted after 315 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -2,5 +2,5 @@ AA BB
|
||||
readonly
|
||||
nobank
|
||||
done
|
||||
Execution halted after 245 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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>
|
||||
run [words] start what was loaded, and tell it those words
|
||||
delete <file> take it off the disk
|
||||
rename <file> <to> call it something else
|
||||
monitor look at memory, change it, and jump into it
|
||||
help this
|
||||
exit stop
|
||||
exit stop, or leave the monitor if you are in it
|
||||
> greeting.txt 17
|
||||
filler1.txt 8
|
||||
filler2.txt 8
|
||||
@@ -21,5 +22,5 @@ aName22CharactersLong! 22
|
||||
12 files
|
||||
> > I do not know: frobnicate
|
||||
> halted
|
||||
Execution halted after 12376 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -19,5 +19,5 @@ CosmOS
|
||||
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.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
CosmOS
|
||||
> loaded, starting at 2000
|
||||
> poem.txt, 0 lines
|
||||
> : : : : > : : > 1: alpha
|
||||
2: INSERTED
|
||||
3: beta
|
||||
4: gamma
|
||||
> : > > 1: CHANGED
|
||||
2: INSERTED
|
||||
3: beta
|
||||
> written, 22 bytes
|
||||
> finished
|
||||
> poem.txt, 3 lines
|
||||
> 1: CHANGED
|
||||
2: INSERTED
|
||||
3: beta
|
||||
> there is no such line
|
||||
> finished
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,17 @@
|
||||
CosmOS
|
||||
> one.txt 13
|
||||
two.txt 14
|
||||
2 files
|
||||
> renamed
|
||||
> first.txt 13
|
||||
two.txt 14
|
||||
2 files
|
||||
> gone
|
||||
> two.txt 14
|
||||
1 file
|
||||
> no such file
|
||||
> rename what to what?
|
||||
> there is no such file, or that name is taken
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -5,5 +5,5 @@ finished
|
||||
> Hello, World!
|
||||
finished
|
||||
> halted
|
||||
Execution halted after 2668 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
CosmOS
|
||||
> loaded, starting at 2000
|
||||
> keys, by interrupt. q stops.
|
||||
ab
|
||||
the console has been handed back
|
||||
finished
|
||||
> > keys, by interrupt. q stops.
|
||||
cd
|
||||
the console has been handed back
|
||||
finished
|
||||
> > x examine, d disassemble, s set, b bank, g go, exit leaves
|
||||
* bank 00
|
||||
* FE00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
FE10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
FE20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
FE30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
* > halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -869,5 +869,5 @@ the board has settled
|
||||
finished
|
||||
>
|
||||
halted
|
||||
Execution halted after 11671206 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -21,5 +21,5 @@ stopped
|
||||
finished
|
||||
> >
|
||||
halted
|
||||
Execution halted after 26219 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
CosmOS
|
||||
> x examine, d disassemble, s set, b bank, g go, exit leaves
|
||||
* b <program|data|number>
|
||||
* there is no such bank
|
||||
* loaded, starting at 2000
|
||||
* bank 00
|
||||
* 2000 47 00 10 00 SETD.0 1000
|
||||
2004 18 10 SWI 10
|
||||
2006 47 00 10 44 SETD.0 1044
|
||||
200A 18 10 SWI 10
|
||||
200C 47 00 10 7A SETD.0 107A
|
||||
2010 27 1F INIB 1F
|
||||
2012 18 11 SWI 11
|
||||
2014 47 00 10 5D SETD.0 105D
|
||||
* 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 ................
|
||||
* bank 01
|
||||
* 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
|
||||
* bank 02
|
||||
* 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 ................
|
||||
* Fault: The device on port 233 refused the access at Program Address 0x1359, and nothing is installed to deal with it.
|
||||
Execution halted.
|
||||
[exit 1]
|
||||
@@ -3,5 +3,5 @@ no filesystem on the disk
|
||||
> no filesystem on the disk
|
||||
>
|
||||
halted
|
||||
Execution halted after 636 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -4,8 +4,10 @@ CosmOS
|
||||
hello.sbx 52
|
||||
Life.sbx 1411
|
||||
Snake.sbx 2175
|
||||
Keys.sbx 663
|
||||
Say.sbx 155
|
||||
notes.txt 21
|
||||
5 files
|
||||
7 files
|
||||
> load what?
|
||||
> no such file
|
||||
> not a program
|
||||
@@ -17,5 +19,5 @@ finished
|
||||
what should I call you? hello, Claude. that is all I do.
|
||||
finished
|
||||
> halted
|
||||
Execution halted after 12778 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
CosmOS
|
||||
> loaded, starting at 2000
|
||||
> nothing was said
|
||||
finished
|
||||
> it says: notes.txt
|
||||
finished
|
||||
> it says: a longer thing with spaces
|
||||
finished
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,12 @@
|
||||
CosmOS
|
||||
> loaded, starting at 2000
|
||||
> saved it
|
||||
read it back, 22 bytes:
|
||||
a file kept by asking
|
||||
renamed it
|
||||
deleted it
|
||||
and it is gone
|
||||
finished
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -289,5 +289,5 @@ you ran into something
|
||||
finished
|
||||
>
|
||||
halted
|
||||
Execution halted after 1910669 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
ABCZ
|
||||
Execution halted after 21 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
O
|
||||
K
|
||||
!
|
||||
Execution halted after 19 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
04
|
||||
04
|
||||
06
|
||||
Execution halted after 114 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
00
|
||||
from the disk
|
||||
02
|
||||
Execution halted after 245 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -2,5 +2,5 @@ one
|
||||
two
|
||||
three
|
||||
done
|
||||
Execution halted after 126 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
O
|
||||
K
|
||||
Execution halted after 17 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Fault: 0xFE at Program Address 0x0002 is not an instruction.
|
||||
Execution halted after 2 cycles.
|
||||
Execution halted.
|
||||
[exit 1]
|
||||
|
||||
@@ -4,5 +4,5 @@ caught 01FC
|
||||
read ok
|
||||
lowered ok
|
||||
fenced 05
|
||||
Execution halted after 535 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Hello, World!
|
||||
Execution halted after 70 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Input Test: Will echo anything you put in.
|
||||
Hello SplitBit
|
||||
Execution halted after 406 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Input Test: Will echo anything you put in.
|
||||
Hello SplitBit
|
||||
Execution halted after 406 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
00 00 00 00 01
|
||||
1
|
||||
Execution halted after 4707 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ready
|
||||
trap
|
||||
device
|
||||
Execution halted after 105 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
OKC
|
||||
Execution halted after 19 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
loader
|
||||
loaded off a disk, with a string and a loop of its own
|
||||
Execution halted after 1098 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
before
|
||||
loaded
|
||||
back
|
||||
Execution halted after 132 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Fault: The device on port 16 interrupted at Program Address 0x0019, and hardware vector 16 has no handler installed.
|
||||
M
|
||||
S
|
||||
Execution halted after 16 cycles.
|
||||
Execution halted.
|
||||
[exit 1]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
0000 0032
|
||||
Execution halted after 1134 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
AAA
|
||||
Execution halted after 24 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
00 31
|
||||
Execution halted after 78 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -3,5 +3,5 @@ twenty
|
||||
sixty three
|
||||
automatic
|
||||
done
|
||||
Execution halted after 272 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Hello
|
||||
World
|
||||
H
|
||||
Execution halted after 79 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Hello, World!
|
||||
42 is the great answer.
|
||||
Execution halted after 295 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -9,5 +9,5 @@ Testing printByteHex...
|
||||
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF
|
||||
|
||||
Testing complete!
|
||||
Execution halted after 71185 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Fault: The device on port 17 refused the access at Program Address 0x000A, and nothing is installed to deal with it.
|
||||
O
|
||||
Execution halted after 6 cycles.
|
||||
Execution halted.
|
||||
[exit 1]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
caught
|
||||
caught
|
||||
done
|
||||
Execution halted after 136 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -3,5 +3,5 @@ flags 01
|
||||
5A 5A
|
||||
refused
|
||||
refused
|
||||
Execution halted after 443 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
10 10 00
|
||||
FF 01 00
|
||||
05 00 00
|
||||
Execution halted after 440 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
here.txt 0002 already here
|
||||
doc.txt 0003 first draft
|
||||
doc.txt 0004 a second draft, which is longer than the first
|
||||
notes.txt 0004 a second draft, which is longer than the first
|
||||
doc.txt gone
|
||||
notes.txt gone
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -3,5 +3,5 @@ across.txt 0002 BC ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHI
|
||||
aName22CharactersLong! 0000 16 exactly twenty two!!!!
|
||||
empty.txt 0000 00
|
||||
absent.txt missing
|
||||
Execution halted after 19230 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -11,5 +11,5 @@ across.txt 700
|
||||
empty.txt 0
|
||||
aName22CharactersLong! 22
|
||||
files: 12
|
||||
Execution halted after 9498 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
here.txt 0002 already here
|
||||
first.txt 0003 written by SplitBit itself
|
||||
second.txt 0004 and a second one after it
|
||||
Execution halted after 5888 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
quiet: 7
|
||||
answer: 42
|
||||
pointer: ABC
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -1,3 +1,3 @@
|
||||
OK
|
||||
Execution halted after 14 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -5,5 +5,5 @@ reclaimed: FFFF
|
||||
still good: 33
|
||||
borrowed: 7 and 9
|
||||
back home: FFFF
|
||||
Execution halted after 1054 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -2,5 +2,5 @@ one
|
||||
two
|
||||
three
|
||||
AFTER
|
||||
Execution halted after 122 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ADD OR and NOP
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -1,4 +1,4 @@
|
||||
Fault: Software vector 20, dispatched from Program Address 0x0008, has no handler installed.
|
||||
O
|
||||
Execution halted after 5 cycles.
|
||||
Execution halted.
|
||||
[exit 1]
|
||||
|
||||
@@ -4,5 +4,5 @@ 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.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Two pointers, no stack shenanigans.
|
||||
Execution halted after 395 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
OK!
|
||||
good
|
||||
AFTER
|
||||
Execution halted after 77 cycles.
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
load Edit.sbx
|
||||
run poem.txt
|
||||
a
|
||||
alpha
|
||||
beta
|
||||
gamma
|
||||
.
|
||||
i 2
|
||||
INSERTED
|
||||
.
|
||||
l
|
||||
c 1
|
||||
CHANGED
|
||||
d 4
|
||||
l
|
||||
w
|
||||
q
|
||||
run poem.txt
|
||||
l
|
||||
d 99
|
||||
q
|
||||
exit
|
||||
@@ -0,0 +1,9 @@
|
||||
dir
|
||||
rename one.txt first.txt
|
||||
dir
|
||||
delete first.txt
|
||||
dir
|
||||
delete first.txt
|
||||
rename two.txt
|
||||
rename two.txt two.txt
|
||||
exit
|
||||
@@ -0,0 +1,3 @@
|
||||
load Files.sbx
|
||||
run
|
||||
exit
|
||||
@@ -0,0 +1,10 @@
|
||||
load Keys.sbx
|
||||
run
|
||||
abq
|
||||
run
|
||||
cdq
|
||||
monitor
|
||||
b program
|
||||
x fe00
|
||||
exit
|
||||
exit
|
||||
@@ -0,0 +1,18 @@
|
||||
monitor
|
||||
b nonsense
|
||||
b 9
|
||||
load greet.sbx
|
||||
b program
|
||||
d 2000
|
||||
x 2000
|
||||
b data
|
||||
x 1000
|
||||
b 2
|
||||
x 0
|
||||
s 8000 26 48 D1 00 26 0A D1 00 18 12
|
||||
d 8000
|
||||
g 8000
|
||||
d 8000
|
||||
exit
|
||||
dir
|
||||
exit
|
||||
@@ -0,0 +1,5 @@
|
||||
load Say.sbx
|
||||
run
|
||||
run notes.txt
|
||||
run a longer thing with spaces
|
||||
exit
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user