From 0b6d2be43f232b4af6ae8eec003137039952e6c4 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Wed, 19 Aug 2026 18:19:15 -0400 Subject: [PATCH] CosmOS: a service interface for the disk and console, and the monitor in the shell Two changes that arrived together because both live in cosmos.asm. THE SERVICES. A loaded program that wanted a file had to include the whole filesystem, carrying 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. Five services are added at pinned numbers 20 to 24: osFileRead, osFileSave, osFileDelete, osFileRename and osPrintNumber. The 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, going out it is A and B together, and neither direction needs a record in memory whose shape both sides must agree on. There is deliberately no service to mount a disk. The system mounts one before its first prompt, and a program mounting it again was only ever a consequence of owning a second copy of the library, so that call disappears rather than moving. Apps/Files.asm writes, reads, renames and deletes a file in 645 bytes and includes nothing but the service names. THE MONITOR. Previously an application, now part of the shell, because an application occupies the one region a loaded application is given: a monitor that was an application could never examine another one, since loading the thing to be inspected would replace the thing doing the inspecting. "monitor" turns it on and the prompt becomes "*". It is a mode rather than a sub-prompt, and it persists: because the mode is a variable the prompt reads rather than a second loop, and every path back to the prompt goes through one place including osExit, a program started with "g" that gives the machine back arrives at the monitor prompt it was started from. Examining a program and running it therefore do not interrupt each other. "exit" leaves whatever you are in. It supersedes dump, and adds disassembly, writing bytes, and jumping to an address. Its instruction table is generated from the assembler's own list by Tests/instructiontable.py rather than typed again, and Tests/docs.sh checks both that the system's copy matches the generator and that the lengths that table implies are the ones the manual's Bytes column prints. A disassembler that disagreed about a length would not print one line wrong, it would lose its place and print everything after it wrong. Also here: b refuses a bank that is not registered, since asking the controller for one is refused and a refusal nobody catches stops the machine; g records the Stack the way run does, without which a program returning through osExit restored whatever the last run had left; and make cosmos-disk now depends on the system as well as the image. Co-Authored-By: Claude Opus 5 --- Programs/CosmOS/Apps/Files.asm | 176 ++++++ Programs/CosmOS/Source/cosmos.asm | 929 ++++++++++++++++++++++++++-- Programs/CosmOS/Source/services.asm | 26 + Programs/makefile | 5 +- README.md | 4 +- SplitBit Programming Manual.md | 86 ++- Tests/docs.sh | 91 ++- Tests/expected/cosmos.out | 5 +- Tests/expected/cosmosKeys.out | 6 +- Tests/expected/cosmosMonitor.out | 31 + Tests/expected/cosmosServices.out | 12 + Tests/input/cosmosFiles2.in | 3 + Tests/input/cosmosKeys.in | 5 +- Tests/input/cosmosMonitor.in | 18 + Tests/instructiontable.py | 60 ++ Tests/makedisks.sh | 8 + Tests/manifest | 18 + 17 files changed, 1411 insertions(+), 72 deletions(-) create mode 100644 Programs/CosmOS/Apps/Files.asm create mode 100644 Tests/expected/cosmosMonitor.out create mode 100644 Tests/expected/cosmosServices.out create mode 100644 Tests/input/cosmosFiles2.in create mode 100644 Tests/input/cosmosMonitor.in create mode 100755 Tests/instructiontable.py diff --git a/Programs/CosmOS/Apps/Files.asm b/Programs/CosmOS/Apps/Files.asm new file mode 100644 index 0000000..5adf173 --- /dev/null +++ b/Programs/CosmOS/Apps/Files.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 diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 825adc6..00fb3d2 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -67,8 +67,26 @@ bootNoDisk: ; ---- The loop ---- +; ---- The loop ---- +; +; The shell has two modes and one prompt that says which. Ordinary mode runs programs; +; monitor mode also looks at memory, changes it, and jumps into it. +; +; THE MODE IS A VARIABLE RATHER THAN A SECOND LOOP, and that is what makes it persistent +; without anything having to remember it. Every way back here goes through this one place, +; INCLUDING A PROGRAM GIVING THE MACHINE BACK - so jumping to an address, letting it run, +; and having it exit puts you back at the monitor prompt you left from, rather than at the +; shell. Only saying so leaves the monitor, or a program breaking the machine badly enough +; to need starting again. prompt: + SETD.0 Mode + LDA.0 + BRA promptPlain + SETD.0 MonitorPrompt + BRI promptSay +promptPlain: SETD.0 PromptText +promptSay: CALL printString SETD.0 CommandLine @@ -104,11 +122,6 @@ prompt: CALL textSame BRQ doRun - SETD.0 CommandLine - SETD.1 DumpName - CALL textSame - BRQ doDump - SETD.0 CommandLine SETD.1 DeleteName CALL textSame @@ -127,8 +140,46 @@ prompt: SETD.0 CommandLine SETD.1 ExitName CALL textSame - BRQ quit + BRQ doExit + SETD.0 CommandLine + SETD.1 MonitorName + CALL textSame + BRQ doMonitor + + ; The monitor's own commands, which only answer when the monitor is on. They are single + ; letters because they are typed constantly and because the prompt has already said which + ; mode you are in; the plain shell keeps its words and stays plain. + SETD.0 Mode + LDA.0 + BRA promptUnknown + + SETD.0 CommandLine + SETD.1 ExamineName + CALL textSame + BRQ doExamine + + SETD.0 CommandLine + SETD.1 DisName + CALL textSame + BRQ doDisassemble + + SETD.0 CommandLine + SETD.1 SetName + CALL textSame + BRQ doSet + + SETD.0 CommandLine + SETD.1 BankName + CALL textSame + BRQ doBank + + SETD.0 CommandLine + SETD.1 GoName + CALL textSame + BRQ doGo + +promptUnknown: ; Nothing matched. Saying which word was not understood is worth the four instructions: ; it tells somebody who mistyped what they actually typed. SETD.0 Unknown @@ -141,6 +192,26 @@ prompt: ; Running out of console leaves the cursor part way along a line, because there was no ; return at the end to move it on. Somebody who typed "exit" has already pressed one, and ; a second would only leave a blank line behind. +; Leaving whatever you are in: the monitor if you are in it, and the machine if you are +; not. Two exits to stop from the monitor, which is what every nested prompt has ever asked +; for and reads the right way round. +doExit: + SETD.0 Mode + LDA.0 + BRA quit + RSTA + STA.0 + BRI prompt + +doMonitor: + INIA 0x01 + SETD.0 Mode + STA.0 + SETD.0 MonitorHelp + CALL printString + CALL newLine + BRI prompt + quitRanOut: CALL newLine quit: @@ -787,6 +858,144 @@ handleArgument: CALL copyText RETI +; ---- The disk, on a program's behalf ---- +; +; A loaded program that wanted a file used to include the whole filesystem, so it carried a +; private copy of code the system already has running, and mounted a disk that was already +; mounted. These are that code, reachable through a number instead. +; +; Every one of them answers in Q, and the answer is written into the frame over the saved +; register, because RETI puts every register back and would otherwise throw it away. That +; has to be done here rather than in a routine of its own: the offsets are from where the +; Stack Pointer is, and a CALL moves it by ten. +; +; A machine with no disk answers no to all of them rather than going ahead and finding out, +; because sbfs on a disk that was never mounted is reading whatever bank 3 happens to be. + +; DP0 names the file, DP1 says where to put it. Q is zero if it read, and DP3 comes back +; holding how many bytes there were. +handleFileRead: + SETD.2 DiskReady + LDA.2 + BRA fileReadNo + + CALL sbfsFind + BNQ fileReadNo + + ; A file of 256 blocks is 64K, which will not fit in Data Memory and will not fit in the + ; pointer that says how long it is either. Refused, rather than read as much of as fits: + ; a length that lies is worse than a file that will not open. + SETD.2 SbfsFileBlocks + LDA.2 + BNA fileReadNo + + CALL sbfsRead + BNQ fileReadNo + + ; How long it is: the block count is the high byte of that and the tail is the low one, + ; which is how a size is put together everywhere on this disk. + SETD.2 SbfsFileBlocks + INCD.2 + LDA.2 + SETD.2 SbfsFileTail + LDB.2 + + MVSD.2 + DPUP.2 0d05 ; The saved DP3, high byte first. + STA.2 + INCD.2 + STB.2 + + MVSD.2 + DPUP.2 0d02 ; And the saved Q. + RSTA + STA.2 + RETI + +fileReadNo: + MVSD.2 + DPUP.2 0d02 + INIA 0d1 + STA.2 + RETI + +; DP0 names the file, DP1 is the bytes, and A and B together are how many. Q is zero if it +; saved. Whether it was there before makes no difference, which is what saving means. +handleFileSave: + SETD.2 DiskReady + PSHA + LDA.2 + BRA fileSaveNoDisk + POPA + + ; Blocks are the high half of the count and the tail is the low half. + SETD.2 SbfsFileBlocks + PSHA + RSTA + STA.2 ; A whole file's block count fits in a byte, so this is zero. + INCD.2 + POPA + STA.2 + SETD.2 SbfsFileTail + STB.2 + + CALL sbfsSaveFile + MVQA + MVSD.2 + DPUP.2 0d02 + STA.2 + RETI + +fileSaveNoDisk: + POPA + MVSD.2 + DPUP.2 0d02 + INIA 0d1 + STA.2 + RETI + +; DP0 names it. Q is zero if it went. +handleFileDelete: + SETD.2 DiskReady + LDA.2 + BRA serviceNoDisk + CALL sbfsDelete + MVQA + MVSD.2 + DPUP.2 0d02 + STA.2 + RETI + +; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved. +handleFileRename: + SETD.2 DiskReady + LDA.2 + BRA serviceNoDisk + CALL sbfsRename + MVQA + MVSD.2 + DPUP.2 0d02 + STA.2 + RETI + +serviceNoDisk: + MVSD.2 + DPUP.2 0d02 + INIA 0d1 + STA.2 + RETI + +; A and B together are a number. Prints it in decimal without leading zeroes, which covers +; a line number and a byte count both, so there is no need for one service each. +handlePrintNumber: + SETD.0 PrintNumber + STA.0 + INCD.0 + STB.0 + SETD.0 PrintNumber + CALL printWordDecimal + RETI + ; Giving the machine back. This is the one place MVDS earns its keep. The program's Stack, ; and the frame this very interrupt arrived on, are both abandoned where they lie, because ; nothing is going to return through either of them. @@ -828,49 +1037,99 @@ handleExit: ; Program Memory and this can is the whole point: the instruction set has no way to look ; at itself, and the controller does, so a monitor is possible at all only through it. -doDump: +; b +; +; The two banks that always exist have names, because "program" is what somebody means and +; 0 is only what the machine calls it. Anything else is a number, and has to be one that is +; really there. +doBank: SETD.1 TextRest LDD.0.1 LDA.0 - BRA dumpGo ; Nothing said, so carry on from where the last one stopped. + BRA bankWhat - ; Which bank. The two that always exist have names, because typing "program" is what - ; somebody means and 0 is what the machine calls it. - CALL textSplit SETD.1 ProgramWord CALL textSame - BRQ dumpBankProgram + BRQ bankProgram SETD.1 DataWord CALL textSame - BRQ dumpBankData + BRQ bankData CALL textHexWord - BNQ dumpBadWhere + BNQ bankWhat SETD.0 TextValue INCD.0 LDA.0 - BRI dumpSetBank -dumpBankProgram: + BRI bankSet +bankProgram: RSTA - BRI dumpSetBank -dumpBankData: + BRI bankSet +bankData: INIA 0d1 -dumpSetBank: +bankSet: + ; The old one is kept, because asking about a bank means writing it down first - and if + ; it turns out not to exist, being left pointed at it would fault on the very next look. + SETD.0 DumpBank + LDB.0 + SETD.1 BankWas + STB.1 SETD.0 DumpBank STA.0 - ; And where in it. Naming a bank without an address means the start of it, which is the - ; only answer that does not depend on what was asked for last time. + ; Naming a bank puts the cursor at the start of it, which is the only answer that does + ; not depend on what was asked for last time. + CALL bankPresent + BRQ bankNotThere + RSTA SETD.0 DumpAt STA.0 INCD.0 STA.0 + SETD.0 BankIs + CALL printString + SETD.0 DumpBank + LDA.0 + CALL printByteHex + CALL newLine + BRI prompt + +bankNotThere: + SETD.0 BankWas + LDA.0 + SETD.1 DumpBank + STA.1 ; Back where it was, which is somewhere that exists. + SETD.0 NoSuchBank + CALL printString + CALL newLine + BRI prompt + +bankWhat: + SETD.0 BankUsage + CALL printString + CALL newLine + BRI prompt + +; x [address] - sixty four bytes. d [address] - eight instructions. Without an address +; either carries on from where the last one stopped, so reading through memory is one +; letter at a time and the two share a place in it. +doExamine: + RSTA + SETD.0 ShowAsCode + STA.0 + BRI showAt + +doDisassemble: + INIA 0x01 + SETD.0 ShowAsCode + STA.0 + +showAt: SETD.1 TextRest LDD.0.1 LDA.0 - BRA dumpCheckBank + BRA dumpGo ; Nothing said, so carry on from where the last one stopped. CALL textHexWord BNQ dumpBadWhere SETD.0 TextValue @@ -885,36 +1144,14 @@ dumpSetBank: STA.1 dumpCheckBank: - ; Is there such a bank? Asking the controller for a bank that is not there is refused, - ; and a refusal nobody catches stops the machine, which is a poor answer to a typing - ; mistake. The bank table says what exists, and it lives in bank 2. - ; - ; Bank n's record starts at n times eight. A and B are a shift register sixteen bits - ; wide, so putting the number in the low half and rotating left three times multiplies - ; it by eight without anything falling off the top: the most it can reach is 2040. - RSTA - SETD.0 DumpBank - LDB.0 - SHL SHL SHL - SETD.0 DumpRecord - STA.0 - INCD.0 - STB.0 - - INIA 0d2 - OUTA 0xE0 ; SourceBank: the controller's own memory. - SETD.0 DumpRecord - LDA.0 - OUTA 0xE1 - INCD.0 - LDA.0 - OUTA 0xE2 - INA 0xE9 ; The flags byte of that bank's record. - INIB 0x01 - AND - BRQ dumpNoBank ; The present bit is down, so nothing is there. + CALL bankPresent + BRQ dumpNoBank dumpGo: + SETD.0 ShowAsCode + LDA.0 + BNA disassembleGo + INIA 0d4 SETD.0 DumpRows STA.0 @@ -1009,8 +1246,21 @@ dumpRowNext: BNA dumpRow BRI prompt +disassembleGo: + INIA 0d8 + SETD.0 DumpRows + STA.0 +disassembleOne: + CALL showInstruction + SETD.0 DumpRows + LDA.0 + DECA + STA.0 + BNA disassembleOne + BRI prompt + dumpBadWhere: - SETD.0 DumpUsage + SETD.0 ExamineUsage CALL printString CALL newLine BRI prompt @@ -1020,6 +1270,434 @@ dumpNoBank: CALL newLine BRI prompt +; s
... +; +; Writes into whichever bank is being looked at, THROUGH THE CONTROLLER, so Program Memory +; can be changed as easily as Data - which no instruction on this machine can do, and which +; is most of the reason for having a monitor at all. +; +; The cursor is left alone. Somebody poking a byte is usually looking at something else, and +; having the address they were reading move underneath them would be a poor reward. +doSet: + SETD.1 TextRest + LDD.0.1 + CALL textHexWord + BNQ setWhat + + SETD.1 DumpBank + LDA.1 + OUTA 0xE3 + SETD.1 TextValue + LDA.1 + OUTA 0xE4 + INCD.1 + LDA.1 + OUTA 0xE5 + + CALL stepPastNumber + PSHD.3 + POPD.0 +setByte: + CALL textHexWord + BNQ prompt ; Nothing more on the line, so that was all of them. + SETD.1 TextValue + INCD.1 + LDA.1 + OUTA 0xE9 ; The destination steps on by itself, so a run of bytes is a loop. + CALL stepPastNumber + PSHD.3 + POPD.0 + BRI setByte + +setWhat: + SETD.0 SetUsage + CALL printString + CALL newLine + BRI prompt + +; g
+; +; Somewhere to go. It does not come back by itself - that would want a breakpoint, which is +; a byte written over an instruction and a handler waiting for it, and neither exists yet. +; But a program that gives the machine back the ordinary way lands at the prompt it was +; started from, which is this one, still in the monitor. +doGo: + SETD.1 TextRest + LDD.0.1 + CALL textHexWord + BNQ goWhat + + ; Where the Stack is now, written down before leaving, exactly as run does it. Whatever is + ; jumped to may give the machine back through osExit, and osExit puts the Stack back to + ; what is written here - so without this it would restore the one the LAST run left, or + ; none at all, and the shell would come back with its Stack pointing at nothing. + MVSD.0 + SETD.1 SystemStack + STD.0.1 + + SETD.1 TextValue + LDD.3.1 + BRD.3 + +goWhat: + SETD.0 GoUsage + CALL printString + CALL newLine + BRI prompt + +; DP0 names text that textHexWord has just read a number off the front of. LEAVES DP3 past +; the digits and any spaces after them, ready for the next one. +; +; DP3 rather than DP0, because a subroutine cannot hand a pointer back in DP0: CALL saves it +; and RET puts it back, so stepping it here would be undone on the way out. +stepPastNumber: + PSHD.0 + POPD.3 + SETD.1 TextDigits + LDB.1 +stepPastDigits: + BRB stepPastSpaces + INCD.3 + DECB + BRI stepPastDigits +stepPastSpaces: + LDA.3 + BRA stepPastDone + INIB 0x20 + XOR + BNQ stepPastDone + INCD.3 + BRI stepPastSpaces +stepPastDone: + RET + + +; Points the controller's source at the cursor, so that reading port 0xE9 walks forwards. +aimAtDumpAt: + SETD.0 DumpBank + LDA.0 + OUTA 0xE0 + SETD.0 DumpAt + LDA.0 + OUTA 0xE1 + INCD.0 + LDA.0 + OUTA 0xE2 + RET + +; One byte from the cursor, and the cursor moves on. +; +; THE BYTE COMES BACK IN Q, not in A, because a subroutine cannot hand anything back in A: +; CALL saves it and RET puts it back, so an assignment here would be undone by the return. +takeByte: + INA 0xE9 + PSHA + SETD.0 DumpAt + INCD.0 + LDA.0 + INCA + STA.0 + BNC takeByteDone + DECD.0 + LDA.0 + INCA + STA.0 +takeByteDone: + POPA + RSTB + OR + RET + +; ---- Showing instructions ---- +; +; The half of a monitor that a byte dump cannot do. What it needs and a dump does not +; is to know how LONG each instruction is, because getting that wrong does not print one +; line wrong - it loses the place and prints everything after it wrong. + +; One instruction: where it is, the bytes it is made of, and what it says. +showInstruction: + SETD.0 DumpAt + LDA.0 + CALL printByteHex + INCD.0 + LDA.0 + CALL printByteHex + INIA 0x20 + OUTA 0x00 + INIA 0x20 + OUTA 0x00 + + CALL aimAtDumpAt + CALL takeByte + MVQA + SETD.0 Opcode + STA.0 + + CALL findInstruction + BNQ showUnknown + + ; What shape it is, and from that how many bytes it runs to. + PSHD.3 + POPD.0 + INCD.0 + LDA.0 + SETD.1 Shape + STA.1 + + SETD.0 ShapeLength + LDB.1 +shapeStep: + BRB shapeGot + INCD.0 + DECB + BRI shapeStep +shapeGot: + LDA.0 + SETD.1 Length + STA.1 + + ; The rest of its bytes. The first one is already read. + SETD.0 InstrBytes + SETD.1 Opcode + LDA.1 + STA.0 + INCD.0 + SETD.1 Length + LDB.1 + DECB +readRest: + BRB readRestDone + PSHB + CALL takeByte + POPB + MVQA + STA.0 + INCD.0 + DECB + BRI readRest +readRestDone: + + ; Show them, padded out so that what follows lines up however long the instruction was. + SETD.0 InstrBytes + SETD.1 Length + LDB.1 +showBytes: + PSHB + LDA.0 + CALL printByteHex + INIA 0x20 + OUTA 0x00 + POPB + INCD.0 + DECB + BNB showBytes + + INIB 0d4 + SETD.0 Length + LDA.0 +padBytes: + CCF + SUB + BRQ padDone ; As many as there are, so nothing to pad. + PSHA + PSHB + INIA 0x20 + OUTA 0x00 + OUTA 0x00 + OUTA 0x00 + POPB + POPA + DECB + BRI padBytes +padDone: + INIA 0x20 + OUTA 0x00 + + ; Its name, without the spaces it is padded to four with. + PSHD.3 + POPD.0 + DPUP.0 0d02 + INIB 0d4 +showName: + LDA.0 + BRA showNameDone + PSHB + INIB 0x20 + XOR + POPB + BRQ showNameDone + OUTA 0x00 + INCD.0 + DECB + BNB showName +showNameDone: + + ; And whatever follows it, which depends only on the shape. + SETD.0 Shape + LDA.0 + BRA showOperandNone ; 0, nothing at all + + DECA + BRA showAddress ; 1, an address + DECA + BRA showByte ; 2, one byte + DECA + BRA showSelector ; 3, a Data Pointer + DECA + BRA showSelectorByte ; 4, a Data Pointer and a byte + DECA + BRA showSelectorAddress ; 5, a Data Pointer and an address + BRI showTwoSelectors ; 6 + +showOperandNone: + CALL newLine + RET + +showAddress: + INIA 0x20 + OUTA 0x00 + SETD.0 InstrBytes + INCD.0 + LDA.0 + CALL printByteHex + INCD.0 + LDA.0 + CALL printByteHex + CALL newLine + RET + +showByte: + INIA 0x20 + OUTA 0x00 + SETD.0 InstrBytes + INCD.0 + LDA.0 + CALL printByteHex + CALL newLine + RET + +showSelector: + CALL putSelectorOne + CALL newLine + RET + +showSelectorByte: + CALL putSelectorOne + INIA 0x20 + OUTA 0x00 + SETD.0 InstrBytes + DPUP.0 0d02 + LDA.0 + CALL printByteHex + CALL newLine + RET + +showSelectorAddress: + CALL putSelectorOne + INIA 0x20 + OUTA 0x00 + SETD.0 InstrBytes + DPUP.0 0d02 + LDA.0 + CALL printByteHex + INCD.0 + LDA.0 + CALL printByteHex + CALL newLine + RET + +showTwoSelectors: + CALL putSelectorOne + INIA 0d46 ; . + OUTA 0x00 + SETD.0 InstrBytes + DPUP.0 0d02 + LDA.0 + CALL printDecimalDigit + CALL newLine + RET + +; ".n" for the selector that follows the opcode. +putSelectorOne: + INIA 0d46 + OUTA 0x00 + SETD.0 InstrBytes + INCD.0 + LDA.0 + CALL printDecimalDigit + RET + +; A byte that decodes as nothing. Shown as it is, and the cursor moves on by one, because +; the only honest thing to do with a byte that is not an instruction is say so and carry on. +showUnknown: + SETD.0 Opcode + LDA.0 + CALL printByteHex + SETD.0 UnknownText + SWI osPrintString + RET + +; Looks the opcode up. DP3 lands on its entry and Q is zero, or Q is not zero and it is not +; an instruction at all. +findInstruction: + SETD.3 Instructions + SETD.0 InstructionCount + LDB.0 +findStep: + LDA.3 + SETD.0 Opcode + PSHB + LDB.0 + XOR + POPB + BRQ findFound + DPUP.3 0d07 + DECB + BNB findStep + RSTA + INIB 0d1 + CCF + ADD + RET +findFound: + RSTA + RSTB + CCF + ADD + RET + +; Is there such a bank? Q is zero if there is not. +; +; ASKING THE CONTROLLER FOR A BANK THAT IS NOT THERE IS REFUSED, and a refusal nobody +; catches stops the machine, which is a wretched answer to a mistyped number. The bank table +; says what exists, and it lives in bank 2. +; +; Bank n's record starts at n times eight. A and B are a shift register sixteen bits wide, +; so putting the number in the low half and rotating left three times multiplies it by +; eight without anything falling off the top: the most it can reach is 2040. +bankPresent: + RSTA + SETD.0 DumpBank + LDB.0 + SHL SHL SHL + SETD.0 DumpRecord + STA.0 + INCD.0 + STB.0 + + INIA 0d2 + OUTA 0xE0 ; SourceBank: the controller's own memory. + SETD.0 DumpRecord + LDA.0 + OUTA 0xE1 + INCD.0 + LDA.0 + OUTA 0xE2 + INA 0xE9 ; The flags byte of that bank's record. + INIB 0x01 + AND + RET + ; ---- help ---- doHelp: @@ -1029,6 +1707,15 @@ doHelp: SETD.0 HelpMoreText CALL printString CALL newLine + + ; And what the monitor adds, but only when it is on. Listing commands that would not + ; answer is a way of teaching somebody something untrue. + SETD.0 Mode + LDA.0 + BRA prompt + SETD.0 MonitorHelp + CALL printString + CALL newLine BRI prompt #Data @@ -1057,21 +1744,18 @@ run [words] start what was loaded, and tell it those words delete take it off the disk rename call it something else" HelpMoreText: -"dump sixty four bytes of memory, and again for more - dump
+"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" -DumpUsage: -"dump
" +ExamineUsage: +"x
, or x on its own to carry on" NoSuchBank: "there is no such bank" ProgramWord: "program" DataWord: "data" -DumpName: -"dump" ExecMagic: "SBEX" @@ -1104,6 +1788,33 @@ NothingLoaded: Finished: "finished" +MonitorPrompt: +"* " +UnknownText: +" ? +" +MonitorName: +"monitor" +MonitorHelp: +"x examine, d disassemble, s set, b bank, g go, exit leaves" +ExamineName: +"x" +DisName: +"d" +SetName: +"s" +BankName: +"b" +GoName: +"g" +BankUsage: +"b " +BankIs: +"bank " +SetUsage: +"s
..." +GoUsage: +"g
" DirName: "dir" LoadName: @@ -1139,6 +1850,11 @@ RenameFrom: RunArgument: #Reserve 0d64 +; A number on its way to being printed, since the routine that prints one wants it in +; memory and a service is handed it in registers. +PrintNumber: + 0x00 0x00 + ; ---- The vectors a loaded program brought with it ---- ; ; Six bytes each: where it goes, what goes there, and what was there before. The last two @@ -1164,10 +1880,102 @@ DumpAt: 0x00 0x00 DumpRows: 0x00 +Mode: + 0x00 DumpCount: 0x00 +BankWas: + 0x00 +ShowAsCode: + 0x00 DumpRecord: 0x00 0x00 +Opcode: + 0x00 +Shape: + 0x00 +Length: + 0x00 +InstrBytes: + #Reserve 0d4 + +; How many bytes an instruction of each shape runs to, the opcode included. +ShapeLength: + 0d1 0d3 0d2 0d2 0d3 0d4 0d3 + +InstructionCount: + 0d64 + +; ---- The instruction table ---- +; +; Generated by Tests/instructiontable.py from the assembler's own list, and checked against +; it by Tests/docs.sh. Seven bytes each: the opcode, the shape, and four characters of name +; with the zero the assembler puts after a string. +Instructions: + 0x00 0d0 "ADD " + 0x01 0d0 "SUB " + 0x02 0d0 "AND " + 0x03 0d0 "OR " + 0x04 0d0 "XOR " + 0x05 0d0 "NOTA" + 0x06 0d0 "NOTB" + 0x07 0d0 "SHL " + 0x08 0d0 "SHR " + 0x10 0d1 "BRI " + 0x11 0d1 "BRQ " + 0x12 0d1 "BRA " + 0x13 0d1 "BRB " + 0x14 0d1 "BRC " + 0x15 0d3 "BRD " + 0x1A 0d1 "BNQ " + 0x1B 0d1 "BNA " + 0x1C 0d1 "BNB " + 0x1D 0d1 "BNC " + 0x17 0d1 "CALL" + 0x18 0d2 "SWI " + 0x19 0d0 "RETI" + 0x1F 0d0 "RET " + 0x20 0d0 "RSTA" + 0x21 0d0 "RSTB" + 0x22 0d0 "INCA" + 0x23 0d0 "INCB" + 0x24 0d0 "DECA" + 0x25 0d0 "DECB" + 0x26 0d2 "INIA" + 0x27 0d2 "INIB" + 0x28 0d0 "CCF " + 0x29 0d0 "MVQA" + 0x2A 0d0 "MVQB" + 0x2B 0d0 "SIF " + 0x2C 0d0 "CIF " + 0x30 0d0 "PSHQ" + 0x31 0d0 "PSHA" + 0x32 0d0 "PSHB" + 0x33 0d3 "PSHD" + 0x34 0d0 "POPA" + 0x35 0d0 "POPB" + 0x36 0d3 "POPD" + 0x40 0d3 "INCD" + 0x41 0d3 "DECD" + 0x42 0d3 "LDA " + 0x43 0d3 "LDB " + 0x44 0d3 "STQ " + 0x45 0d3 "STA " + 0x46 0d3 "STB " + 0x47 0d5 "SETD" + 0x48 0d4 "DPUP" + 0x49 0d4 "DPDN" + 0x4A 0d6 "LDD " + 0x4B 0d6 "STD " + 0x4C 0d3 "MVSD" + 0x4D 0d3 "MVDS" + 0xD0 0d2 "OUTQ" + 0xD1 0d2 "OUTA" + 0xD2 0d2 "OUTB" + 0xE0 0d2 "INA " + 0xE1 0d2 "INB " + 0xF0 0d0 "NOP " + 0xFF 0d0 "HALT" DumpBytes: #Reserve 0d16 @@ -1193,4 +2001,9 @@ CommandLine: osReadLine handleReadLine osExit handleExit osArgument handleArgument + osFileRead handleFileRead + osFileSave handleFileSave + osFileDelete handleFileDelete + osFileRename handleFileRename + osPrintNumber handlePrintNumber Device 0x20 diskDone diff --git a/Programs/CosmOS/Source/services.asm b/Programs/CosmOS/Source/services.asm index dd11f44..382456f 100644 --- a/Programs/CosmOS/Source/services.asm +++ b/Programs/CosmOS/Source/services.asm @@ -25,3 +25,29 @@ 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 diff --git a/Programs/makefile b/Programs/makefile index b7933ae..636bd5f 100644 --- a/Programs/makefile +++ b/Programs/makefile @@ -79,7 +79,10 @@ $(COSMOS_DISK): $(APPS) $(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) diff --git a/README.md b/README.md index 8d02ee9..e69a800 100644 --- a/README.md +++ b/README.md @@ -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 \ \: 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: diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 2c25f95..1060787 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -555,6 +555,11 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w | 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 @@ -563,6 +568,18 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w 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. @@ -642,7 +659,7 @@ Whoever does the loading keeps its own code and data below the addresses the loa ## Programs That Come With The System: -`Programs/CosmOS/Apps` holds what the shell can load. Most of them are old programs that were written for the bare machine and needed five edits to become loadable ones; the last three were written for the system as it is now. +`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 | | --- | --- | @@ -650,12 +667,79 @@ Whoever does the loading keeps its own code and data below the addresses the loa | 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. diff --git a/Tests/docs.sh b/Tests/docs.sh index b841f40..d208e50 100755 --- a/Tests/docs.sh +++ b/Tests/docs.sh @@ -184,17 +184,98 @@ else: # 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") -offered = re.findall(r'^\s{2}(os[A-Za-z]+)\s+0d\d+', services, re.M) -if not offered: +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] - for name in offered: - if ("| %s |" % name) not in section: - problems.append("%s is a service and has no row in the services table" % name) + 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))): diff --git a/Tests/expected/cosmos.out b/Tests/expected/cosmos.out index df5ec0f..7cf852a 100644 --- a/Tests/expected/cosmos.out +++ b/Tests/expected/cosmos.out @@ -4,10 +4,9 @@ load read a program off the disk run [words] start what was loaded, and tell it those words delete take it off the disk rename call it something else -dump sixty four bytes of memory, and again for more - dump
+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 diff --git a/Tests/expected/cosmosKeys.out b/Tests/expected/cosmosKeys.out index 2b63bd9..cf26779 100644 --- a/Tests/expected/cosmosKeys.out +++ b/Tests/expected/cosmosKeys.out @@ -8,10 +8,12 @@ finished cd the console has been handed back finished -> > FE00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +> > 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 +* > halted Execution halted. [exit 0] diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out new file mode 100644 index 0000000..beffb84 --- /dev/null +++ b/Tests/expected/cosmosMonitor.out @@ -0,0 +1,31 @@ +CosmOS +> x examine, d disassemble, s set, b bank, g go, exit leaves +* b +* 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] diff --git a/Tests/expected/cosmosServices.out b/Tests/expected/cosmosServices.out new file mode 100644 index 0000000..82b12f3 --- /dev/null +++ b/Tests/expected/cosmosServices.out @@ -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] diff --git a/Tests/input/cosmosFiles2.in b/Tests/input/cosmosFiles2.in new file mode 100644 index 0000000..1cec294 --- /dev/null +++ b/Tests/input/cosmosFiles2.in @@ -0,0 +1,3 @@ +load Files.sbx +run +exit diff --git a/Tests/input/cosmosKeys.in b/Tests/input/cosmosKeys.in index 8598921..0b9bf40 100644 --- a/Tests/input/cosmosKeys.in +++ b/Tests/input/cosmosKeys.in @@ -3,5 +3,8 @@ run abq run cdq -dump program fe00 +monitor +b program +x fe00 +exit exit diff --git a/Tests/input/cosmosMonitor.in b/Tests/input/cosmosMonitor.in new file mode 100644 index 0000000..9fd7abd --- /dev/null +++ b/Tests/input/cosmosMonitor.in @@ -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 diff --git a/Tests/instructiontable.py b/Tests/instructiontable.py new file mode 100755 index 0000000..88379ae --- /dev/null +++ b/Tests/instructiontable.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""The instruction table, as the assembler has it. + +The monitor needs the same 64 instructions the assembler does, with the same names and the +same lengths, and a disassembler that disagreed with the assembler about how long an +instruction is would not merely print one thing wrong - it would lose its place and print +everything after it wrong too. So the table is generated from assembly.c rather than typed +out again, and Tests/docs.sh checks the generated form against what is in the monitor. + +Shapes are what follows the opcode: + 0 nothing 1 an address 2 one byte 3 a Data Pointer selector + 4 selector, byte 5 selector, address 6 two selectors +""" +import re +import sys + +ADDRESS = {0x10, 0x11, 0x12, 0x13, 0x14, 0x17, 0x1A, 0x1B, 0x1C, 0x1D} +ONE_BYTE = {0x18, 0x26, 0x27} +TWO_SELECTORS = {0x4A, 0x4B} +SELECTOR = {0x15, 0x33, 0x36, 0x40, 0x41, 0x42, 0x43, 0x44, + 0x45, 0x46, 0x47, 0x48, 0x49, 0x4C, 0x4D} + + +def shapeOf(opcode): + if opcode in TWO_SELECTORS: + return 6 + if opcode == 0x47: # SETD, a selector and then an address + return 5 + if opcode in (0x48, 0x49): # DPUP and DPDN, a selector and then a byte + return 4 + if opcode in SELECTOR: + return 3 + if opcode in ADDRESS: + return 1 + if opcode in ONE_BYTE or (opcode & 0xF0) in (0xD0, 0xE0): + return 2 + return 0 + + +def table(path="Source/Assembler/assembly.c"): + source = open(path).read() + found = re.findall(r'\{0x([0-9A-Fa-f]{2}),\s*"([A-Z0-9]+)"\}', source) + return [(int(code, 16), name) for code, name in found] + + +def asAssembly(entries): + lines = [] + for opcode, name in entries: + padded = (name + " ")[:4] + lines.append(' 0x%02X 0d%d "%s"' % (opcode, shapeOf(opcode), padded)) + return lines + + +if __name__ == "__main__": + entries = table() + if len(sys.argv) > 1 and sys.argv[1] == "--count": + print(len(entries)) + else: + for line in asAssembly(entries): + print(line) diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 5d838a9..60415fa 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -125,3 +125,11 @@ printf 'the second one' > two.txt "$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \ "$ROOT/Programs/CosmOS/Apps/Edit.asm" -o "$WORK/Edit.sbx" >/dev/null "$TOOL" put "$DISKS/editor.img" "$WORK/Edit.sbx" >/dev/null + +# A disk for the file services, holding nothing but the program that exercises them. Its +# own, because that program writes: it tidies up after itself, but a run that stopped part +# way would leave a document behind on a fixture every later test reads. +"$TOOL" format "$DISKS/services.img" 256 2 >/dev/null +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Apps/Files.asm" -o "$WORK/Files.sbx" >/dev/null +"$TOOL" put "$DISKS/services.img" "$WORK/Files.sbx" >/dev/null diff --git a/Tests/manifest b/Tests/manifest index b9e4d5a..a63fd29 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -211,6 +211,17 @@ cosmosNoDisk | CosmOS/Source/cosmos.asm | run | cosmosNoD # load can refuse is tried first, and run is asked for twice, so the Stack being reclaimed # rather than merely abandoned is what makes the second one work. cosmosRun | CosmOS/Source/cosmos.asm | run | cosmosRun.in | - | disks/cosmos.img +# The monitor, which is part of the shell rather than a program: a mode you go into and stay +# in. The targets are chosen to be stable - a loaded program's code and data, and the bank +# table - rather than the system's own code, which would churn whenever any library changed. +# +# Looking at the bank table is worth having on its own. It is the machine describing itself, +# and it shows the disk buffer that sbfsMount registered as bank 3 at boot. +# +# The last part is what the mode is FOR: a program typed in as bytes, run with g, and the +# prompt that comes back is the monitor's own. A program giving the machine back lands where +# it was started from, so looking at something and running it do not interrupt each other. +cosmosMonitor | CosmOS/Source/cosmos.asm | run | cosmosMonitor.in | - | disks/cosmos.img # The original hello.asm, brought over as an application. It is not much of a program, # but it is the one that talks to the hardware directly: it writes to port 0x00 instead # of calling osPrintString, so it is the case where a program reaches past the system and @@ -266,6 +277,12 @@ cosmosSay | CosmOS/Source/cosmos.asm | run | cosmosSay # split a file into lines, edit them, build a file back out of them, and save it over # something that was already there and is now a different size. cosmosEdit | CosmOS/Source/cosmos.asm | run | cosmosEdit.in | - | disks/editor.img +# The file services, exercised by a program that includes NOTHING but the service names: no +# filesystem library, no console library. It writes a file, reads it back, says how long it +# was, renames it and deletes it, in 645 bytes - against the editor's 4941, which does less +# with files and carries the filesystem inside it. That difference is the whole case for the +# service layer, and this is where it is checked rather than argued. +cosmosServices | CosmOS/Source/cosmos.asm | run | cosmosFiles2.in | - | disks/services.img # The programs CosmOS loads, checked on their own so that a failure here reads as "the app # does not assemble" rather than as a broken disk image. app-greet | CosmOS/Apps/greet.asm | assemble | - | - @@ -275,6 +292,7 @@ app-Snake | CosmOS/Apps/Snake.asm | assemble | - app-Keys | CosmOS/Apps/Keys.asm | assemble | - | - app-Say | CosmOS/Apps/Say.asm | assemble | - | - app-Edit | CosmOS/Apps/Edit.asm | assemble | - | - +app-Files | CosmOS/Apps/Files.asm | assemble | - | - # ---- Programs driven by console input ---- inputTest | inputTest.asm | run | inputTest.in | -