; cosmos.asm ; CosmOS, and the shell that is most of it. ; ; The machine boots into this. It registers what the hardware brought, mounts whatever ; disk is attached, and then reads lines and does what they say until there is no more ; typing to be had. ; ; ---- Where things live ---- ; ; The system keeps to the bottom of both memories, and everything above is for whatever ; it is running: ; ; Program Memory 0x0000 - 0x4FFF the system ; 0x5000 - a loaded program's code ; Data Memory 0x0000 - 0x2FFF the system ; 0x3000 - a loaded program's data ; ; Nothing enforces that. Nothing can: the fence guards a range, and this is a convention ; about which range belongs to whom rather than a rule about what may be touched. The ; assembler prints both segment sizes, and they are what to watch. ; ; A program is staged at 0x8000 while it is being loaded, which is inside the region a ; loaded program will own. That is safe because nothing is running during a load, and it ; is where a big program can be read without the system reserving the room for good. ; ; ---- What it can do ---- ; ; dir List what is in the current directory. ; cd Go somewhere else, or to the root when told nothing. ; mkdir Make a directory. rmdir Remove an empty one. ; delete Remove a file. rename Give one another name. ; load Read a program off the disk and put it where it asks to go. ; run Start the program that was loaded. ; help Say what these are. ; exit Stop, or leave the monitor if that is where you are. ; ; Anything else that is not one of those is looked for as a program and run if it is ; found, so most of what the machine does is not in this list at all. ; ; The dispatch below is a chain of comparisons, which is the right shape for five commands ; and the wrong shape for twenty; it is eleven now, and when it grows the table that ; dispatchTest.asm demonstrates is where it should go. ; ; Written by Anachronaut #Include console.asm #Include text.asm #Include sbfs.asm #Include services.asm #Include script.asm #Program boot: ; ---- The screen this system wants ---- ; ; Eighty columns, because that is what CosmOS was written for: its own help text is ; seventy-four characters wide, and dir, the monitor and the assembler's messages all ; assume room. A machine wakes up in the smaller mode, which is right for a machine - it ; is the system that knows what shape of screen its own output needs. ; ; Harmless where there is no screen. Writing to a port nothing answers on does nothing, ; so this is one instruction wasted on a machine with a terminal instead. INIA 0x01 OUTA 0x31 ; And a cursor, so that a person can see where the next thing they type will go. A machine ; wakes up without one, which is right: a program painting its own screen does not want one ; blinking in the middle of it. A system that reads lines from a person does. INIA 0x04 OUTA 0x02 SETD.0 Banner CALL printString CALL newLine ; Find out whether there is a filesystem to talk to. Doing this once at boot rather than ; once per command means a disk swapped underneath us is not noticed, which is honest ; for a machine whose disk is a file named on the command line. CALL sbfsMountAll SETD.0 DiskReady BNQ bootNoDisk INIA 0x01 STA.0 ; ---- Saying that this start arrived ---- ; ; The loader marks the disk before handing over, and nothing clears it but this. A system ; that crashes on the way here leaves the mark, and the loader finding it still set next ; time is how a machine that will not start says so. ; ; HERE RATHER THAN LATER, and the threshold is the whole of what the mark means. It is ; not a claim that anything works - a prompt can be reached by something broken in every ; other way. It is the point where somebody can type, which is what the fallback exists ; to give back: anything wrong past here can be fixed from the prompt, and nothing wrong ; before it can be fixed at all. CALL sbfsBootState BNQ bootStartup SETD.0 SbfsStateWas LDA.0 INIB 0d1 XOR BRQ bootArrived INIB 0d2 XOR BNQ bootStartup ; Started by the fallback, so the system somebody asked for is not the one running. Said ; once, here, because there is nowhere else it would be noticed. SETD.0 OnFallback CALL printString BRI bootStartup bootArrived: RSTA CALL sbfsSetBootState BRI bootStartup bootNoDisk: RSTA STA.0 SETD.0 NoDisk CALL printString CALL newLine ; ---- Something to run before anybody types ---- ; ; Every way of arriving at the prompt for the first time comes through here, which is the ; point: a startup script should run whether the disk was marked, unmarked, or is not there ; at all - and in the last case scriptOpen simply finds nothing. ; ; A MISSING ONE IS NOT A FAULT and says nothing, because a clean install has none and a ; machine that complained at every boot about a file nobody wrote would be teaching its owner ; to ignore it. A file that is THERE and is not a script is the other case entirely: somebody ; meant it to run. bootStartup: SETD.0 StartupName CALL scriptOpen BRQ bootReady MVQA INIB 0x01 CCF SUB BRQ bootReady ; There is none, which is ordinary. SETD.0 StartupNotOne CALL printString CALL newLine bootReady: ; ---- 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: ; ---- A script stops at the first line that did not work ---- ; ; Checked here, before the next line is read, because this is the one place every command ; comes back to. A build whose first step failed and whose second step ran anyway produces ; something wrong and says it succeeded, which is the failure this whole flag exists to ; prevent. SETD.1 ScriptDepth LDA.1 BRA promptWhere SETD.1 LineFailed LDA.1 BRA promptWhere SETD.0 ScriptStopped CALL printString CALL newLine ; Every level, not just this one. A build whose helper script failed should not carry on ; in the script that called the helper either. CALL scriptAbandon promptWhere: ; ---- The prompt is said by whoever turns out to be supplying the line ---- ; ; Not here, which is where it was and where it cannot be right. The decision has to be made ; before the line is read and the answer is not known until after: a quiet script's #quiet ; is itself a line, so the prompt for it went out before anybody knew to stay silent, and ; the line AFTER a quiet script's last one comes from the console and had already been ; denied its prompt. Both were off by exactly one line and in opposite directions. ; ; So shellReadLine says it. It is the one place that knows. SETD.0 CommandLine INIB 0d127 CALL shellReadLine ; Running out of typing is how this ends. It is not the same as an empty line, which is ; just somebody pressing return, and the shell should sit there when that happens. SETD.0 ConsoleEndOfInput LDA.0 BNA quitRanOut ; ---- How this line went, assumed good until something says otherwise ---- ; ; Cleared here rather than set at the end of each command, and that is what makes this ; affordable. There are thirty seven ways back to this prompt and only fourteen of them ; are failures, so marking the failures costs fourteen lines and marking the successes ; would cost twenty three - and the twenty three would have to be found again every time ; a command grew a new way to finish. A command that says nothing worked. ; ; A PROGRAM SETS THIS ITSELF, from handleExit, and it runs after this point - so what a ; program made of its work is what stands, not the zero written here before it started. SETD.1 LineFailed RSTA STA.1 SETD.0 CommandLine CALL textSplit ; An empty line asks for nothing. SETD.0 CommandLine LDA.0 BRA prompt SETD.0 CommandLine SETD.1 DirName CALL textSame BRQ doDir SETD.0 CommandLine SETD.1 LoadName CALL textSame BRQ doLoad SETD.0 CommandLine SETD.1 RunName CALL textSame BRQ doRun SETD.0 CommandLine SETD.1 CdName CALL textSame BRQ doCd SETD.0 CommandLine SETD.1 MkdirName CALL textSame BRQ doMkdir SETD.0 CommandLine SETD.1 RmdirName CALL textSame BRQ doRmdir SETD.0 CommandLine SETD.1 DeleteName CALL textSame BRQ doDelete SETD.0 CommandLine SETD.1 RenameName CALL textSame BRQ doRename SETD.0 CommandLine SETD.1 DriveName CALL textSame BRQ doDrive SETD.0 CommandLine SETD.1 ClearName CALL textSame BRQ doClear SETD.0 CommandLine SETD.1 EchoName CALL textSame BRQ doEcho SETD.0 CommandLine SETD.1 DoName CALL textSame BRQ doScript SETD.0 CommandLine SETD.1 HelpName CALL textSame BRQ doHelp SETD.0 CommandLine SETD.1 ExitName CALL textSame 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 SETD.0 CommandLine SETD.1 AsmName2 CALL textSame BRQ doAssemble ; ---- And a word the monitor does not know either ---- ; ; It goes where the shell's unknown words go, which is the disk, because the monitor is a ; MODE of the shell rather than a different program and everything the shell can do is ; still supposed to work in it. ; ; THERE WAS NOTHING HERE AT ALL, and the fall through was into sayPrompt - a routine, which ; ended in a RET that had nothing of its own to return to. So it took whatever the Stack ; held and went there. Sometimes that was 0x0003, in the middle of newLine, and the machine ; stopped on a byte that is not an instruction; once it was inside sbfsFormat, and the ; machine formatted the disk it had booted from. The tell was a SECOND prompt printed ; before the wreckage, which is sayPrompt doing exactly what it is for on its way past. BRI promptUnknown ; Where you are, and then the prompt itself. A routine rather than a run of code in the loop, ; because the loop is no longer the only thing that needs it - see shellReadTyped. sayPrompt: ; Where you are, but only when that is not obvious. At the root the prompt is the one it ; has always been, so a machine nobody has moved about on looks exactly as it did - and ; every recorded test that never says "cd" keeps its recorded prompt. SETD.0 SbfsCwd LDA.0 INCD.0 LDB.0 OR BRQ sayPromptMode CALL shellPath SETD.0 CwdAt LDD.0.0 CALL printString sayPromptMode: SETD.0 Mode LDA.0 BRA sayPromptPlain SETD.0 MonitorPrompt BRI sayPromptSay sayPromptPlain: SETD.0 PromptText sayPromptSay: CALL printString RET ; ---- Where a line comes from ---- ; ; The whole of what a script is. Everything below this - splitting the line, matching it ; against the commands, loading a program - cannot tell the difference and does not have to. ; ; A SCRIPT RUNNING OUT IS NOT THE SAME AS TYPING RUNNING OUT. The console ending means there ; is nobody there and the shell should stop; a script ending means go back to whoever asked ; for it. So the end of a script falls through to the console rather than to the door. shellReadLine: SETD.1 ScriptDepth LDA.1 BRA shellReadTyped CALL scriptLine BNQ shellReadLine ; That one ended. Ask again: something under it may still be ; running, and only depth reaching nought means the console. ; The prompt and the echo are one thing - together they look like somebody typing - so a ; quiet script gets neither, and everything else gets both in that order. SETD.1 ScriptQuiet LDA.1 BNA shellReadQuiet CALL sayPrompt CALL printString CALL newLine shellReadQuiet: RET shellReadTyped: CALL sayPrompt CALL editLine RET ; ---- The same, for a caller that prints a prompt of its own ---- ; ; The monitor's assembler shows the address it is about to write to and then asks for a line, ; so it must not also get the shell's. It still wants the line to come from the script when ; there is one, which is what lets a script carry a block of assembly. shellReadRaw: SETD.1 ScriptDepth LDA.1 BRA shellReadRawTyped CALL scriptLine BNQ shellReadRaw SETD.1 ScriptQuiet LDA.1 BNA shellReadRawQuiet CALL printString CALL newLine shellReadRawQuiet: RET shellReadRawTyped: CALL editLine RET ; ---- The line the person at the keyboard is typing ---- ; ; THREE DIFFERENT THINGS USED TO DO THIS JOB and which one you got depended on where the ; machine was running. On a terminal the host held the line and did the echoing and the ; backspacing; behind a window the console's own gatherer did it; from a file nothing did it ; at all. One job, three implementations, and none of them here - which is why there was no ; way to move about in a line and nowhere for a history to live. ; ; So the shell does it. The console delivers keys and says nothing about what they mean, the ; same way the disk says what a drive is and nothing about what should be on it. Where the ; cursor goes and what the line looks like afterwards are decisions, and decisions belong to ; whoever is reading. ; ; DP0 names a buffer and B says how many characters it holds, not counting the zero byte ; that ends it. Q is how long the line turned out to be. That is readLine's bargain exactly, ; so this drops in where that was called. ; ; ConsoleEndOfInput is set if the console ran out instead of a line being finished, which is ; readLine's other promise and the one the shell uses to know when to stop. ; ; ---- Two doors, because the history is the SHELL'S ---- ; ; editLine is what the shell reads through and keeps what it is given. editLinePlain is what ; a program reads through, and does not: Edit would otherwise fill the history with the text ; of somebody's document, and pressing Up in the middle of writing one would put "dir" in ; it. A program gets the editing, which is what it wanted; the history belongs to the thing ; whose lines are commands. ; ; Two entry points rather than a flag a caller sets first, because a caller cannot forget to ; do this one. ; ; ---- Why most keystrokes draw nothing but themselves ---- ; ; A character typed at the END of a line needs no cursor moved: printing it is the whole of ; the change. That is the case almost every keystroke is, and it matters beyond speed - ; moving the cursor by hand means writing the console's cursor registers, which a terminal ; is told about in an escape sequence. Redrawing on every keypress would fill every recorded ; transcript in the test suite with them. So the cheap path is the common one, and the line ; is only reprinted when something happened in the middle of it. editLine: INIA 0x01 BRI editLineBegin editLinePlain: RSTA editLineBegin: SETD.1 EditKeepHistory STA.1 SETD.1 EditBase STD.0.1 SETD.1 EditRoom STB.1 RSTA SETD.1 EditLength STA.1 SETD.1 EditAt STA.1 SETD.1 EditDrawn STA.1 SETD.1 ConsoleEndOfInput STA.1 ; Where the line begins, ASKED rather than assumed. The prompt has just been printed and ; only the console knows where it left off. INA 0x03 SETD.1 EditRow STA.1 INA 0x04 SETD.1 EditColumn STA.1 ; How wide the screen is, which is what decides where a long line carries on. A machine ; with nothing on that port answers zero, and a width of zero would make the walking below ; never finish, so it is taken as the width this system asks for at boot. INA 0x32 BNA editWidthKnown INIA 0d80 editWidthKnown: SETD.1 EditWidth STA.1 ; ---- How the console was found, so it can be put back that way ---- ; ; Not "line mode with a cursor", which is only how the SHELL keeps it. A program that had ; asked for key mode and then read a line through the system would have been handed back a ; console in line mode, having asked for nothing of the sort. ; ; The status port reports every one of the three things the control port can ask for, and ; reports them in the same order two bits along - so one shift turns what the console IS ; into what to write to make it that again. INA 0x01 RSTB SHR SHR INIB 0d7 AND MVQA SETD.1 EditWasControl STA.1 ; Key mode with a cursor, and INTERRUPTS OFF whatever they were. Nothing echoes from here ; on: everything that appears below is put there by this routine. Interrupts are off ; because this is about to block on the data port, and the manual is explicit that a ; program does one or the other and not both. INIA 0x05 OUTA 0x02 editKey: INA 0x00 SETD.1 EditChar STA.1 ; The ways out first, since they leave rather than change anything. XOR leaves the answer ; in Q and A alone, so one read stands for the whole ladder. INIB 0x0A XOR BRQ editDone INIB 0x0D XOR BRQ editDone INIB 0xFF XOR BRQ editEnded ; ---- And the key that means there is no more ---- ; ; Ctrl-D, which the terminal used to turn into the end of input all by itself and cannot any ; more: that is a thing a terminal does while it is holding the line, and it is not holding ; it now. So the shell does it, which is the same trade as the echoing and the backspacing. ; ; ONLY ON AN EMPTY LINE, which is the rule everywhere else it appears. In the middle of ; something typed it means neither "stop" nor a character, so it means nothing. INIB 0x04 XOR BNQ editNotEnd SETD.1 EditLength LDB.1 BRB editEnded editNotEnd: INIB 0x08 XOR BRQ editBack INIB 0x86 XOR BRQ editDelete INIB 0x82 XOR BRQ editLeft INIB 0x83 XOR BRQ editRight INIB 0x84 XOR BRQ editHome INIB 0x85 XOR BRQ editEnd INIB 0x80 XOR BRQ editUp INIB 0x81 XOR BRQ editDown ; Anything else is a character if it is printable and nothing at all if it is not. The ; console's own keys that this shell has no use for land here and are ignored rather than ; typed, which is the whole reason they are above ASCII. INIB 0x20 CCF SUB BRC editKey INIB 0x7F CCF SUB BNC editKey ; ---- Putting a character in ---- editInsert: SETD.1 EditLength LDA.1 SETD.1 EditRoom LDB.1 CCF SUB BNC editKey ; Full. The zero byte on the end is not counted in the room. ; A hole at the insertion point, made from the top down so nothing is overwritten before ; it has been moved. SETD.1 EditLength LDA.1 SETD.1 EditAt LDB.1 CCF SUB MVQB ; How many characters are above the insertion point. SETD.1 EditBase LDD.0.1 SETD.1 EditLength LDA.1 DPUA.0 ; One past the last character. editInsertShift: BRB editInsertPut DECD.0 LDA.0 INCD.0 STA.0 DECD.0 DECB BRI editInsertShift editInsertPut: SETD.1 EditChar LDA.1 STA.0 SETD.1 EditLength LDA.1 INCA STA.1 SETD.1 EditAt LDA.1 INCA STA.1 ; At the end of the line, printing it IS the change. SETD.1 EditLength LDB.1 CCF SUB BNQ editInsertRedraw SETD.1 EditChar LDA.1 OUTA 0x00 SETD.1 EditLength LDA.1 SETD.1 EditDrawn STA.1 BRI editKey editInsertRedraw: CALL editRedraw BRI editKey ; ---- Taking one out ---- ; ; Backspace and Delete are different keys doing different things: one takes the character ; BEFORE the insertion point, the other the one under it. They meet at editTakeOut, which is ; the only part they share. editBack: SETD.1 EditAt LDA.1 BRA editKey ; Nothing before it. Rubbing out past the start of the line ; would eat the prompt, which belongs to whoever printed it. DECA STA.1 ; Whether that was the last character decides how it disappears, and it has to be asked ; before the buffer shortens under it. SETD.1 EditLength LDB.1 DECB CCF SUB BNQ editBackMiddle CALL editTakeOut ; Rubbed out where it stands, which is what a terminal has always done for a backspace: no ; cursor moved by hand, and so nothing said to the terminal but three ordinary bytes. INIA 0x08 OUTA 0x00 INIA 0x20 OUTA 0x00 INIA 0x08 OUTA 0x00 SETD.1 EditLength LDA.1 SETD.1 EditDrawn STA.1 BRI editKey editBackMiddle: CALL editTakeOut CALL editRedraw BRI editKey editDelete: SETD.1 EditAt LDA.1 SETD.1 EditLength LDB.1 CCF SUB BRQ editKey ; Nothing under the cursor at the end of a line. CALL editTakeOut CALL editRedraw BRI editKey ; Takes the character at the insertion point out of the buffer and shortens it. Draws ; nothing: what that should look like is the caller's business and the two callers disagree. editTakeOut: SETD.1 EditBase LDD.0.1 SETD.1 EditAt LDA.1 DPUA.0 SETD.1 EditLength LDA.1 SETD.1 EditAt LDB.1 CCF SUB MVQB DECB ; One of those is the character going. editTakeOutShift: BRB editTakeOutDone INCD.0 LDA.0 DECD.0 STA.0 INCD.0 DECB BRI editTakeOutShift editTakeOutDone: SETD.1 EditLength LDA.1 DECA STA.1 RET ; ---- Moving about in it ---- ; ; Nothing on the screen changes, so nothing on the screen is redrawn: only the cursor moves. editLeft: SETD.1 EditAt LDA.1 BRA editKey DECA STA.1 BRI editShow editRight: SETD.1 EditAt LDA.1 SETD.1 EditLength LDB.1 CCF SUB BRQ editKey ; Already at the end. SETD.1 EditAt LDA.1 INCA STA.1 BRI editShow editHome: RSTA SETD.1 EditAt STA.1 BRI editShow editEnd: SETD.1 EditLength LDA.1 SETD.1 EditAt STA.1 BRI editShow ; ---- Backwards and forwards through what was typed before ---- ; ; The line on the screen is replaced outright, so both of these redraw. That is also what ; makes the padding below matter: a long line replaced by a short one leaves the tail of the ; long one behind unless something rubs it out. editUp: SETD.1 EditKeepHistory LDA.1 BRA editKey ; A program's line has no history behind it. SETD.1 HistoryPick LDA.1 BRA editKey ; Already at the oldest one kept. ; Leaving the line being typed for the first time, so it is put somewhere. Down brings it ; back, and a person who pressed Up to look at something gets their line returned rather ; than taken. SETD.1 HistoryCount LDB.1 CCF SUB BNQ editUpMoving SETD.3 HistoryTyped CALL historyPut editUpMoving: SETD.1 HistoryPick LDA.1 DECA STA.1 CALL historySlot BRI editRecall editDown: SETD.1 EditKeepHistory LDA.1 BRA editKey ; A program's line has no history behind it. SETD.1 HistoryPick LDA.1 SETD.1 HistoryCount LDB.1 CCF SUB BRQ editKey ; Already back at the line being typed. SETD.1 HistoryPick LDA.1 INCA STA.1 SETD.1 HistoryCount LDB.1 CCF SUB BRQ editDownTyped SETD.1 HistoryPick LDA.1 CALL historySlot BRI editRecall editDownTyped: SETD.3 HistoryTyped editRecall: CALL historyTake CALL editRedraw BRI editKey editShow: SETD.1 EditAt LDA.1 CALL editPlace BRI editKey ; ---- The end of a line, and the end of the typing ---- editEnded: INIA 0x01 SETD.1 ConsoleEndOfInput STA.1 ; AND NO NEWLINE. Nobody pressed Return, so there is no line to end: the console simply ; stopped having anything to say. Printing one here pushes whatever is said next down a row ; for a reason nobody could see, which is what it did until this line was written. BRI editFinish editDone: ; The newline belongs after the whole line, not after wherever the cursor was left sitting. ; Asked first, because a line finished at its end - which is almost every line - needs no ; cursor moved and so says nothing to the terminal. SETD.1 EditAt LDA.1 SETD.1 EditLength LDB.1 CCF SUB BRQ editDoneEnd LDA.1 CALL editPlace editDoneEnd: CALL newLine ; Kept before the buffer is handed over and while EditLength still says how long it is. ; Only on this path: a console that ran out was not somebody finishing a line. SETD.1 EditKeepHistory LDA.1 BRA editFinish CALL historyAdd editFinish: ; And the console back exactly as it was found. SETD.1 EditWasControl LDA.1 OUTA 0x02 SETD.1 EditBase LDD.0.1 SETD.1 EditLength LDA.1 DPUA.0 RSTA STA.0 ; The zero byte that ends it. SETD.1 EditLength LDA.1 RSTB CCF ADD ; Q is how long the line is. RET ; ---- Putting the line back on the screen ---- ; ; The whole of it, every time, rather than the part that changed. A hundred and twenty odd ; characters is nothing to this machine, and the alternative is four separate cases that all ; have to agree about what is already up there. editRedraw: SETD.1 EditRow LDA.1 OUTA 0x03 SETD.1 EditColumn LDA.1 OUTA 0x04 SETD.1 EditBase LDD.0.1 SETD.1 EditLength LDB.1 editRedrawNext: BRB editRedrawTail LDA.0 OUTA 0x00 INCD.0 DECB BRI editRedrawNext editRedrawTail: ; ---- Rubbing out what is no longer there ---- ; ; One space was enough while the only thing that ever shortened a line was taking one ; character out of it. Pressing Up replaces the whole line, and a long one replaced by a ; short one leaves the tail of the long one sitting on the screen looking like part of ; what you are typing. ; ; So the spaces go on for exactly as far as the line has shrunk, which is one after a ; character was taken out and none at all when one was put in. printSpaces takes none for ; an answer and prints nothing, which is what makes that case cost nothing to allow. SETD.1 EditDrawn LDA.1 SETD.1 EditLength LDB.1 CCF SUB BRC editRedrawGrew ; Borrowed, so the line is longer than what is up there. MVQA BRI editRedrawPad editRedrawGrew: RSTA editRedrawPad: SETD.1 EditPrinted STA.1 CALL printSpaces ; How much went to the screen altogether, which is how far back the start of the line is. SETD.1 EditPrinted LDA.1 SETD.1 EditLength LDB.1 CCF ADD MVQA SETD.1 EditPrinted STA.1 ; And what is up there now is exactly the line. SETD.1 EditLength LDA.1 SETD.1 EditDrawn STA.1 CALL editAnchor SETD.1 EditAt LDA.1 CALL editPlace RET ; ---- Where the line starts, worked out backwards ---- ; ; The console has just been told to print, so it knows where printing ENDED, and the line ; began that many characters before it. Asking afterwards rather than trusting what was ; remembered is what makes this survive the screen SCROLLING: a line printed on the bottom ; row moves everything up by one, and a remembered row would be one too low from then on. editAnchor: INA 0x03 SETD.1 EditWalkRow STA.1 INA 0x04 SETD.1 EditWalkColumn STA.1 SETD.1 EditPrinted LDA.1 SETD.1 EditWalkBack STA.1 editAnchorWalk: SETD.1 EditWalkColumn LDA.1 SETD.1 EditWalkBack LDB.1 CCF SUB BRC editAnchorRowUp ; Further back than this row goes. MVQA SETD.1 EditWalkColumn STA.1 BRI editAnchorDone editAnchorRowUp: ; Off the front of this row, so take what the row used and carry on along the one above. SETD.1 EditWalkBack LDA.1 SETD.1 EditWalkColumn LDB.1 INCB ; The column itself, and the step onto it. CCF SUB MVQA SETD.1 EditWalkBack STA.1 SETD.1 EditWidth LDA.1 DECA SETD.1 EditWalkColumn STA.1 SETD.1 EditWalkRow LDA.1 BRA editAnchorTop DECA STA.1 BRI editAnchorWalk editAnchorTop: ; The line began off the top of the screen, which means more was scrolled away than the ; line is long. There is nothing sensible left to point at, so it starts in the corner. RSTA SETD.1 EditWalkColumn STA.1 editAnchorDone: SETD.1 EditWalkRow LDA.1 SETD.1 EditRow STA.1 SETD.1 EditWalkColumn LDA.1 SETD.1 EditColumn STA.1 RET ; A says how many characters along the line the cursor belongs. Puts it there, carrying onto ; the rows below when the line is longer than one row of the screen. editPlace: SETD.1 EditColumn LDB.1 CCF ADD MVQA SETD.1 EditWalkColumn STA.1 SETD.1 EditRow LDA.1 SETD.1 EditWalkRow STA.1 editPlaceWrap: SETD.1 EditWalkColumn LDA.1 SETD.1 EditWidth LDB.1 CCF SUB BRC editPlaceDone ; Inside the row. MVQA SETD.1 EditWalkColumn STA.1 SETD.1 EditWalkRow LDA.1 INCA STA.1 BRI editPlaceWrap editPlaceDone: SETD.1 EditWalkRow LDA.1 OUTA 0x03 SETD.1 EditWalkColumn LDA.1 OUTA 0x04 RET ; ---- What was typed before ---- ; ; Eight lines, oldest first, in a RING: a ninth pushes the oldest out by moving where the ; ring starts rather than by moving any of the lines. Eight is a power of two, so which slot ; an entry lives in is an AND rather than a division, and nothing is ever copied to make ; room. ; ; A NINTH SLOT HOLDS WHAT WAS BEING TYPED when Up was first pressed, and Down walks back into ; it. Losing a half written line to a keypress is exactly the sort of small rudeness that ; makes a thing unpleasant to use, and it costs one slot to avoid. ; ; The history belongs to the SHELL rather than to the console, which is the whole reason it ; can exist at all: until the keys arrived here, there was nothing to press Up at. ; DP3 to history entry A, where entry nought is the oldest one still kept. historySlot: SETD.1 HistoryStart LDB.1 CCF ADD MVQA INIB 0d7 AND MVQA SETD.3 HistoryLines ; A slot is 128 bytes, and A and B are a sixteen bit shift register: one rotation right ; with B empty turns the slot number into the offset of the slot, high byte and low. RSTB SHR DPUW.3 RET ; Copies the line being edited into the slot DP3 names. historyPut: SETD.1 EditBase LDD.0.1 SETD.1 EditLength LDB.1 historyPutNext: BRB historyPutEnd LDA.0 STA.3 INCD.0 INCD.3 DECB BRI historyPutNext historyPutEnd: RSTA STA.3 RET ; Copies the line in the slot DP3 names into the buffer, and puts the cursor at the end of ; it, which is where somebody who has just recalled a line wants to be. ; ; A LINE TOO LONG FOR THE BUFFER IS CUT SHORT rather than written past the end of it. The ; history is shared with the monitor, which reads into forty bytes where the shell reads ; into a hundred and twenty seven, so this is a real case and not a defensive one. historyTake: SETD.1 EditBase LDD.0.1 RSTA SETD.1 EditLength STA.1 historyTakeNext: SETD.1 EditLength LDA.1 SETD.1 EditRoom LDB.1 CCF SUB BNC historyTakeDone ; No room for another character. LDA.3 BRA historyTakeDone STA.0 INCD.0 INCD.3 SETD.1 EditLength LDA.1 INCA STA.1 BRI historyTakeNext historyTakeDone: RSTA STA.0 SETD.1 EditLength LDA.1 SETD.1 EditAt STA.1 RET ; DP0 and DP3 name strings ending in zero bytes. Q is zero if they are the same. ; ; textSame does this already and takes its two strings in DP0 and DP1, and getting a pointer ; from DP3 into DP1 costs a store, a scratch word and a load. This is shorter than that and ; says what it is doing. historySameAs: LDA.0 LDB.3 XOR BNQ historySameNo LDA.0 BRA historySameYes ; They matched and they both ended. INCD.0 INCD.3 BRI historySameAs historySameNo: INIA 0x01 RSTB CCF ADD ; Q is one. RET historySameYes: RSTA RSTB CCF ADD ; Q is zero. RET ; The line just finished, kept. An empty one is not - that is somebody pressing Return - and ; neither is one the same as the newest already there, because running a command twice ; should not put it in twice. historyAdd: SETD.1 EditLength LDA.1 BRA historyAddDone SETD.1 HistoryCount LDA.1 BRA historyAddPut ; Nothing kept yet, so nothing to be the same as. DECA CALL historySlot SETD.1 EditBase LDD.0.1 CALL historySameAs BRQ historyAddDone historyAddPut: SETD.1 HistoryCount LDA.1 INIB 0d8 CCF SUB BRC historyAddRoom ; Fewer than eight, so there is a slot on the end. ; Full. The new line goes over the oldest and the ring starts one further along, which ; forgets the oldest without moving any of the others. INIA 0d8 CALL historySlot CALL historyPut SETD.1 HistoryStart LDA.1 INCA INIB 0d7 AND MVQA STA.1 BRI historyAddDone historyAddRoom: SETD.1 HistoryCount LDA.1 CALL historySlot CALL historyPut SETD.1 HistoryCount LDA.1 INCA STA.1 historyAddDone: ; However that went, the next Up starts from the newest again. SETD.1 HistoryCount LDA.1 SETD.1 HistoryPick STA.1 RET ; ---- When something goes wrong that nothing can carry on past ---- ; ; A fault used to stop the machine and print a line to whatever was behind it. On a terminal ; that is a diagnosis; behind a window it is a frozen picture and no reason at all, because ; the message goes to a standard error nobody is looking at. The machine looked hung and was ; not - it had stopped, and said so somewhere invisible. ; ; So the system catches all five and says it on the screen instead. ; ; ---- A fault ends the PROGRAM, not the machine ---- ; ; That is the answer to "carry on or start again", and it is not a compromise: a bare RETI ; from most of these meets the very instruction that failed and fails again, so carrying on ; is not on offer. But the machine is almost never what is broken. Everything the shell puts ; back when a program exits - the Stack, the vectors it installed, the drive, the working ; directory, the console, the screen - is exactly what wants putting back after one dies, so ; a fault in a loaded program joins handleExit and you are back at the prompt. ; ; A fault BELOW where programs load is the system's own, and there is nothing to go back to. ; That one says so and stops. ; ; ---- What a handler must not do ---- ; ; Fault. There is no double fault rule on this machine: a handler that commits the fault it ; was called about is called again, forever, and each time costs another frame of Stack. So ; nothing below asks for a service, touches a disk, or reaches anything that can refuse. faultBadOpcode: SETD.0 FaultOpcode BRI faultPlain faultGuard: SETD.0 FaultGuard BRI faultPlain faultBank: SETD.0 FaultBank faultPlain: ; The Stack Pointer names the frame, and it has to be taken before anything pushes. Nothing ; above here does: a SETD and a branch move no Stack. MVSD.3 RSTA SETD.1 FaultNumbered STA.1 BRI faultSay faultNoHandler: SETD.0 FaultNoHandler BRI faultNumbered faultNoDevice: SETD.0 FaultNoDevice faultNumbered: MVSD.3 ; These two are the only faults that say WHICH one, and the machine hands that over in Q - ; the one thing a handler here is given in a register. Kept at once, before printing can ; disturb it. MVQA SETD.1 FaultNumber STA.1 INIA 0x01 SETD.1 FaultNumbered STA.1 faultSay: ; The screen before the words. There is no use saying any of this somewhere it cannot be ; read, and a program that faulted may have left the screen with nowhere to put a letter. CALL faultScreen CALL printString SETD.1 FaultNumbered LDA.1 BRA faultWhere SETD.1 FaultNumber LDA.1 CALL printByteHex faultWhere: SETD.0 FaultAt CALL printString CALL faultAddress CALL newLine ; The registers as the frame kept them, which is what they were when it happened. SETD.0 FaultRegisters CALL printString PSHD.3 POPD.0 DPUP.0 0d3 LDA.0 CALL printByteHex SETD.0 FaultB CALL printString PSHD.3 POPD.0 DPUP.0 0d4 LDA.0 CALL printByteHex SETD.0 FaultQ CALL printString PSHD.3 POPD.0 DPUP.0 0d2 LDA.0 CALL printByteHex CALL newLine ; ---- Whose fault it was ---- ; ; Where it happened says which, and one byte of the address decides it: a loaded program ; begins at 0x4000 and everything below that is the system. PSHD.3 POPD.0 DPUP.0 0d13 LDA.0 INIB 0x50 CCF SUB BRC faultInSystem ; A program that will not be carrying on. handleExit does all of the putting back, and ; takes the status the program is deemed to have stopped with in A. INIA 0x01 SETD.1 FaultStopped STA.1 INIA 0xFF BRI handleExit faultInSystem: ; Nothing to go back to: the shell IS what faulted, and its Stack, its variables and its ; place in its own code are all suspect. Saying so and stopping is the only honest answer, ; and it is a great deal better than the frozen picture this used to be. SETD.0 FaultSystem CALL printString CALL newLine HALT ; The two bytes of the address in the frame, printed high half first. faultAddress: PSHD.3 POPD.0 DPUP.0 0d13 LDA.0 CALL printByteHex INCD.0 LDA.0 CALL printByteHex RET ; ---- A screen this can be read on ---- ; ; "Put the screen in a known mode" is not tidiness. A program that left the screen in bitmap ; mode left nowhere to draw a character at all - the console draws nothing when there are no ; text rows - so without this, the message about what went wrong is invisible, which is the ; one thing it must never be. faultScreen: INIA 0x01 OUTA 0x31 ; Eighty columns of text, whatever was being used. ; And the whole view back to the corner. A scrolled origin or a fraction of a cell puts ; every character somewhere other than where it says it is. RSTA OUTA 0x34 OUTA 0x36 OUTA 0x37 OUTA 0x38 ; ---- Colours it can be read in ---- ; ; A known mode is only half of a known screen: a program that wrote its own palette may ; have left every ink the same as every paper. Attribute one draws in palette entries 16 ; and 17, so those two are written and the rest of the program's colours are left alone - ; there is nothing to be gained here by taking away more than is needed. ; The glyphs first, since a program is as free to redefine a letter as any other tile and ; a message spelled in somebody's tile graphics is no message at all. It touches only the ; console's own tiles, so a program's are left where they are. INIA 0x01 OUTA 0x39 CALL screenBank INIA 0d4 OUTA 0xE3 INIA 0xFC OUTA 0xE4 INIA 0x40 OUTA 0xE5 ; 0xFC40, which is entry sixteen. RSTA OUTA 0xE9 OUTA 0xE9 OUTA 0xE9 OUTA 0xE9 ; Black paper. INIA 0xD0 OUTA 0xE9 INIA 0x40 OUTA 0xE9 INIA 0x38 OUTA 0xE9 RSTA OUTA 0xE9 ; Red ink, the same red the machine wakes up with. INIA 0x01 OUTA 0x06 ; And draw in it. ; A console that can be printed to at all: line mode, a cursor, nothing interrupting. INIA 0x04 OUTA 0x02 RET ; ---- A command that did not work ---- ; ; The one place a failure is recorded, so that the thing reading lines out of a file can ; tell whether to go on. It says nothing: whatever sent us here has already said what was ; wrong in words, and a number after that is noise. See LastStatus. commandFailed: INIA 0x01 SETD.1 LineFailed STA.1 BRI prompt promptUnknown: ; Nothing built in matched, so the disk is asked before anybody is told they are wrong. A ; word this shell does not know is very often the name of a program sitting right there, ; and looking costs a walk of the directory. ; ; THE BUILT-IN COMMANDS ARE TRIED FIRST AND ALWAYS WIN. Nothing that turns up on a disk ; can quietly become "dir" or "exit", which is what makes those two worth trusting at the ; moment the disk is the thing being doubted. SETD.0 DiskReady LDA.0 BRA promptSayUnknown ; No filesystem, so there is nothing to look through. ; THREE PLACES, TRIED IN ORDER: where you are, the system's own place for programs on the ; disk you are on, and then that same place on drive 0. The first is what makes a program ; you are working on the one that runs; the second is what makes Snake work from anywhere ; without a copy of it in every directory; the third is what makes it work from a disk of ; your own, which has your files on it and no system. ; ; None of them is stored anywhere, so there is nothing to configure and nothing to go ; stale. RSTA SETD.0 NamePrefix STA.0 ; Where the person is, kept so it can be given back. A program is fetched from wherever it ; lives and then runs on the files of whoever ran it. INA 0x24 SETD.0 SearchDrive STA.0 promptSearch: CALL nameProgram SETD.0 NameOk LDA.0 BRA promptElsewhere ; Too long to be a path, so it is not the name of one. CALL loadProgram SETD.0 LoadStatus LDA.0 BNA promptNotLoaded ; ---- Loaded, so the drive goes back to the person ---- ; ; The program is in memory now and the block numbers it came from mean nothing any more, ; which is what makes this safe here and not in the middle of a path. A program fetched ; from the system disk then runs on the disk its user was standing on - which is the whole ; point of being able to keep a disk of your own. SETD.0 SearchDrive LDA.0 CALL sbfsUse BRI runLoaded ; It loaded, and the machine is its now. promptNotLoaded: SETD.0 LoadStatus LDA.0 ; No file of that name is not a fault. It is the ordinary case of a word this shell does ; not know, and it is the only answer worth looking somewhere else for. Anything else ; means a file of that name IS there and something is wrong with it, and answering ; "I do not know: Snake" about a Snake.sbx that is sitting on the disk would send ; somebody looking in the wrong place. INIB 0d2 CCF SUB BNQ loadFailed promptElsewhere: ; Was that the last place there is? SETD.0 NamePrefix LDA.0 INIB 0d2 CCF SUB BRQ promptGaveUp ; A word beginning with a separator has said where to look, and looking somewhere else ; would be answering a different question from the one asked. SETD.0 CommandLine LDA.0 INIB 0x2F CCF SUB BRQ promptSayUnknown SETD.0 NamePrefix LDA.0 INCA STA.0 BRI promptSearch promptGaveUp: ; Every place has been tried, and the drive is put back before anybody is told anything: ; the search moved it, and a word the shell does not know should not move somebody either. SETD.0 SearchDrive LDA.0 CALL sbfsUse promptSayUnknown: ; Saying which word was not understood is worth the four instructions: it tells somebody ; who mistyped what they actually typed. SETD.0 Unknown CALL printString SETD.0 CommandLine CALL printString CALL newLine BRI commandFailed ; 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: SETD.0 Farewell CALL printString CALL newLine HALT ; ---- dir ---- ; ; Walks the directory and prints what is in it. A free entry in the middle of a directory ; is stepped over by the walk, so what comes out is the files and nothing else. doDir: SETD.0 DiskReady LDA.0 BRA dirNoDisk RSTA SETD.0 DirSeen STA.0 SETD.0 DirFolders STA.0 CALL sbfsFirst BRI dirCheck dirStep: CALL sbfsNext dirCheck: BNQ dirDone SETD.0 DirSeen LDA.0 INCA STA.0 SETD.0 SbfsName CALL printString SETD.0 SbfsName CALL nameWidth MVQA CALL printSpaces ; A save that stopped before it committed says so instead of saying a size, because the ; size it holds is the room it asked for rather than what was written into it. Asked ; first, since it is the one thing here that is not really a file yet. ; ; IT IS SHOWN RATHER THAN HIDDEN, and that is the whole of the recovery this format ; offers: the bytes are all there under that name, so seeing it is what lets somebody ; rename it back. Left off the listing it would be blocks nobody could account for. SETD.0 SbfsFoundFlags LDA.0 INIB 0x04 AND BNQ dirUnfinished ; A directory says so instead of saying a size. It has no blocks, so the arithmetic ; below would call it a file of no bytes - which is a different thing that happens to ; look the same from here. SETD.0 SbfsFoundFlags LDA.0 INIB 0x02 AND BNQ dirIsDirectory ; A file's length is its block count times 256 plus its tail, which is the block count ; in the high byte and the tail in the low one. Nothing has to multiply anything. ; ; THAT ONLY WORKS WHILE THE BLOCK COUNT FITS IN A BYTE. Two hundred and fifty six blocks ; is sixty five thousand five hundred and thirty six bytes, and the number that comes out ; of the shift is sixteen bits wide - so a file of that size or more came out as itself ; less 65536, which is a plausible number and a wrong one. cosmos.asm is 82,996 bytes and ; this called it 17,460. ; ; Such a file says its size in BLOCKS instead. Printing the true figure would want ; decimal printing twenty four bits wide, which is a page of console.asm to say something ; nobody reads more precisely than "big"; changing the unit says it exactly and can never ; be wrong. SETD.0 SbfsFileBlocks LDA.0 BNA dirInBlocks SETD.0 SbfsFileBlocks INCD.0 LDA.0 SETD.1 DirSize STA.1 SETD.0 SbfsFileTail LDA.0 INCD.1 STA.1 SETD.0 DirSize CALL printWordDecimal CALL newLine BRI dirStep dirInBlocks: SETD.0 SbfsFileBlocks CALL printWordDecimal SETD.0 BlocksText CALL printString CALL newLine BRI dirStep dirUnfinished: SETD.0 UnfinishedText CALL printString CALL newLine BRI dirStep dirIsDirectory: SETD.0 DirFolders LDA.0 INCA STA.0 SETD.0 DirectoryText CALL printString CALL newLine BRI dirStep dirDone: ; Directories were counted alongside the files and now come back out of the total, so ; that "three files" means three files. Saying it any other way makes the number ; disagree with the listing right above it, which is the sort of thing that teaches ; somebody not to trust the listing. SETD.0 DirFolders LDA.0 SETD.1 DirTaken STA.1 SETD.0 DirSeen LDA.0 LDB.1 CCF SUB MVQA STA.0 SETD.0 DirSeen LDA.0 CALL printByteDecimal ; One file is not one files. Cheap to get right and it reads as carelessness otherwise. SETD.0 DirSeen LDA.0 DECA BRA dirOne SETD.0 FilesText BRI dirCount dirOne: SETD.0 FileText dirCount: CALL printString ; And how many of them were directories, but only when there were any. A disk with none ; on it should read exactly the way it always did. SETD.0 DirFolders LDA.0 BRA dirNoFolders SETD.0 AndText CALL printString SETD.0 DirFolders LDA.0 CALL printByteDecimal SETD.0 DirFolders LDA.0 DECA BRA dirOneFolder SETD.0 FoldersText BRI dirFolderCount dirOneFolder: SETD.0 FolderText dirFolderCount: CALL printString dirNoFolders: CALL newLine BRI prompt dirNoDisk: SETD.0 NoDisk CALL printString CALL newLine BRI commandFailed ; DP0 names a string. Q is how many spaces pad it out to twenty four columns. A name ; already that long gets one space, so that it cannot run into the number after it. nameWidth: INIA 0d24 SETD.1 WidthLeft STA.1 widthLoop: LDA.0 BRA widthDone SETD.1 WidthLeft LDA.1 DECA STA.1 BRA widthFloor INCD.0 BRI widthLoop widthFloor: INIA 0d1 SETD.1 WidthLeft STA.1 widthDone: SETD.1 WidthLeft LDA.1 RSTB CCF ADD RET ; ---- Making a file name out of a typed word ---- ; ; Turns what was typed into the name of a file to go and look for. ".sbx" goes on the end ; unless it is already there, so that "Snake" and "Snake.sbx" both find the same file. ; ; THE EXTENSION IS WHAT MAKES A FILE REACHABLE BY NAME. Typing "notes" looks for notes.sbx ; and typing "notes.txt" looks for notes.txt.sbx, so a text file cannot be started by ; typing what it is called, whatever is inside it. Only load reaches a file by its whole ; name, which is why the monitor can still put any file at all in front of itself. ; ; NameOk is one if there is a path in ProgramName and zero if the word could not be made ; into one. What limits it is the buffer, not the format: each NAME along a path is still ; twenty two characters, and the path walker refuses a longer one rather than cutting it ; down. Being refused here reads as an unknown command, which is the truth: nothing this ; shell can reach is called that. nameProgram: RSTA SETD.0 NameOk STA.0 ; Not a name until it turns out to be one. SETD.1 ProgramName INIB 0d59 ; What is left of the buffer, less the four for the extension. ; The system's own place for programs goes on the front, when that is what is being ; tried. Putting it here rather than pasting it on afterwards is what keeps the ".sbx" ; test below looking at the end of the whole thing. SETD.0 NamePrefix LDA.0 BRA nameFromLine ; One is the system's place on this disk; two is the same place on drive 0. INIB 0d2 CCF SUB BRQ nameSystemApps SETD.0 AppsPrefix BRI namePrefixCopy nameSystemApps: SETD.0 SystemAppsPrefix namePrefixCopy: LDA.0 BRA nameFromLine STA.1 INCD.0 INCD.1 DECB BRI namePrefixCopy nameFromLine: SETD.0 CommandLine nameCopy: LDA.0 BRA nameCopied STA.1 INCD.0 INCD.1 DECB BNB nameCopy RET ; Longer than the buffer holds, so it is not a path either. nameCopied: ; DP1 is on the byte after the word, which is where an extension would go, and B is what ; is left. B is written down before anything else wants the registers. RSTA STA.1 SETD.0 NameLeft STB.0 ; Only a word of four characters or more can already end in ".sbx". Stepping back four to ; look at a shorter one would read whatever happens to sit in front of the buffer. INIA 0d55 CCF SUB BRC nameAppend PSHD.1 POPD.2 DPDN.2 0d4 SETD.0 SbxSuffix nameSuffixSame: LDA.0 BRA nameMade ; The suffix ran out with all of it matched. LDB.2 CCF SUB BNQ nameAppend INCD.0 INCD.2 BRI nameSuffixSame nameAppend: SETD.0 NameLeft LDA.0 INIB 0d4 CCF SUB BRC nameDone ; Not four characters of room, so there is no name to be made. ; The suffix carries its own zero, so copying it to the end of the word ends the word. SETD.0 SbxSuffix nameSuffixCopy: LDA.0 STA.1 BRA nameMade INCD.0 INCD.1 BRI nameSuffixCopy nameMade: INIA 0x01 SETD.0 NameOk STA.0 nameDone: RET ; ---- load ---- ; ; Reads a program off the disk and puts it where its header asks to go. Nothing relocates ; anything: the addresses in the header are the ones the program was built for, and it ; would not work anywhere else. ; ; The whole file is staged at 0x8000 first and then blitted into place, because where the ; pieces belong is not known until the header has been read, and the header is in the file. ; ; The command is a thin thing over loadProgram, which is a subroutine because typing a ; program's name loads it too and neither caller should own the loading. What differs ; between them is only what a fault means: "load Snake.sbx" on a disk without it has been ; given a wrong name, and "Snake" on the same disk has typed a word this shell does not ; know. doLoad: SETD.1 TextRest LDD.0.1 LDA.0 BRA loadNothingNamed ; The path exactly as typed. load is how a file is reached by its whole name, so nothing ; is added to it and nothing is assumed about what it ends in. SETD.1 ProgramName INIB 0d64 CALL copyText CALL loadProgram SETD.0 LoadStatus LDA.0 BNA loadFailed SETD.0 LoadedText CALL printString SETD.0 LoadedEntry CALL printWordHex CALL newLine BRI prompt loadFailed: SETD.1 LoadMessage LDD.0.1 CALL printString CALL newLine BRI commandFailed loadNothingNamed: SETD.0 LoadWhat CALL printString CALL newLine BRI commandFailed ; ---- loadProgram ---- ; ; The name is in ProgramName. LoadStatus says what happened and LoadMessage names the text ; for it, because a subroutine cannot hand anything back in a register that a RET puts ; back, and here there are two things to hand back: ; ; 0 loaded, and LoadedEntry says where it starts ; 1 no filesystem on the disk ; 2 no file of that name ; 3 the disk would not read it ; 4 it is not a program ; 5 a version of the format this loader does not know ; 6 more vectors than there is room to keep ; 7 it is a directory ; ; Two is the one worth telling apart from the others. It is the only outcome where nothing ; was wrong with the disk or with a file, and so the only one a caller can fairly report as ; something other than a fault. loadProgram: RSTA SETD.0 LoadStatus STA.0 SETD.0 DiskReady LDA.0 BRA loadNoDisk SETD.0 ProgramName CALL sbfsFind BNQ loadMissing ; A DIRECTORY IS REFUSED HERE AND NOT LEFT TO THE MAGIC CHECK BELOW. It has no blocks, ; so reading it reads nothing and leaves the staging area holding whatever was staged ; last - which, if that was a program, still says "SBEX" and still has a working entry ; address in it. Loading a directory would quietly hand back the program before it, and ; running it would look like the directory had run. SETD.0 SbfsFoundFlags LDA.0 INIB 0x02 AND BNQ loadIsDirectory SETD.1 0x80 0x00 CALL sbfsRead BNQ loadUnreadable ; "SBEX", or this is not a program. Without this, loading a text file would put nonsense ; into Program Memory and then jump into the middle of it. SETD.0 0x80 0x00 SETD.2 ExecMagic INIA 0d4 SETD.1 LoadCount STA.1 loadMagicLoop: LDA.0 LDB.2 XOR BNQ loadNotProgram INCD.0 INCD.2 LDA.1 DECA STA.1 BNA loadMagicLoop ; Version one is code and data. Version two also brings vectors, which is a thing a ; loader has to know how to do rather than a detail it can skip: a program whose handlers ; were quietly dropped would run and then go wrong somewhere with nothing to connect it ; back to here. Anything else is refused. SETD.0 0x80 0x00 DPUP.0 0d04 LDA.0 SETD.1 LoadVersion STA.1 INIB 0d1 XOR BRQ loadVersionKnown SETD.1 LoadVersion LDA.1 INIB 0d2 XOR BNQ loadWrongVersion loadVersionKnown: ; The code. It comes from the staging area just past the sixteen byte header, and goes ; wherever the header says, in Program Memory, which the instruction set cannot write ; and the controller can. INIA 0d1 OUTA 0xE0 ; SourceBank: Data Memory, where the file was staged. INIA 0x80 OUTA 0xE1 INIA 0d16 OUTA 0xE2 ; 0x8010, the first byte after the header. RSTA OUTA 0xE3 ; DestBank: Program Memory. SETD.0 0x80 0x00 DPUP.0 0d06 LDA.0 OUTA 0xE4 INCD.0 LDA.0 OUTA 0xE5 SETD.0 0x80 0x00 DPUP.0 0d10 LDA.0 OUTA 0xE6 INCD.0 LDB.0 OUTB 0xE7 ; ---- Nothing to move is not the same as everything to move ---- ; ; A LENGTH OF ZERO ASKS THE CONTROLLER FOR THE WHOLE 64K, which is the machine's rule and ; a sensible one for a length somebody typed: two bytes cannot say 65536, and a transfer ; of no bytes is not usually what anybody meant. It is exactly what is meant here. A ; segment can genuinely be empty - a five instruction program that only writes to a port ; has no data at all - and this asked to move 65536 bytes into a bank that has not got ; them, so the controller refused and the machine stopped in the middle of loading. ; ; The header says how long the segment is, so the loader knows before it asks. It has ; both bytes of the length in hand here, so testing them costs one instruction. ; ; NOTHING IS LOST BY SKIPPING IT. A blit leaves the controller's addresses past whatever ; it touched, and a blit of nothing would have left them exactly where they are. OR BRQ loadNoCode INIA 0x01 OUTA 0xE8 ; Blit. loadNoCode: ; Then the data. A blit leaves its addresses past whatever it touched, so the source is ; already sitting on the first byte of the data and only the destination changes. INIA 0d1 OUTA 0xE3 ; DestBank: Data Memory. SETD.0 0x80 0x00 DPUP.0 0d12 LDA.0 OUTA 0xE4 INCD.0 LDA.0 OUTA 0xE5 SETD.0 0x80 0x00 DPUP.0 0d14 LDA.0 OUTA 0xE6 INCD.0 LDB.0 OUTB 0xE7 ; And the same for the data, which is the segment that is actually empty in practice. OR BRQ loadNoData INIA 0x01 OUTA 0xE8 ; Blit. loadNoData: ; ---- The vectors it brought ---- ; ; Kept here rather than installed. A vector points into a program, so it has no business ; being in the table while that program is only loaded and not running: run puts them in ; and exit takes them out again, so the window they are live in is exactly the run. ; Keeping our own copy is also what lets a program be run more than once, since the ; staging area it came in on is fair game for the program's own use. ; ; Where to read them from is not worked out. The data blit left the controller's source ; address on the first byte after the data, which is where they are, so it is read back. SETD.0 VectorSource INA 0xE1 STA.0 INCD.0 INA 0xE2 STA.0 SETD.0 0x80 0x00 DPUP.0 0d05 LDA.0 SETD.1 LoadedVectorCount STA.1 BRA loadVectorsCopied ; More than there is room for is refused rather than half taken. Half a program's ; handlers is not a smaller version of that program. INIB 0d17 CCF SUB BNC loadTooManyVectors SETD.0 VectorSource LDD.2.0 ; DP2 walks the entries where they are staged. SETD.3 LoadedVectors ; DP3 walks our own copy of them. SETD.1 LoadedVectorCount LDA.1 SETD.1 VectorsLeft STA.1 loadVectorCopy: ; Four bytes: where it goes, then what goes there. The two bytes for what was there ; before are left alone until something is actually put in. LDA.2 STA.3 INCD.2 INCD.3 LDA.2 STA.3 INCD.2 INCD.3 LDA.2 STA.3 INCD.2 INCD.3 LDA.2 STA.3 INCD.2 INCD.3 INCD.3 INCD.3 SETD.1 VectorsLeft LDA.1 DECA STA.1 BNA loadVectorCopy loadVectorsCopied: ; Where it starts. Written out by hand rather than through a routine, because a routine ; could not hand two bytes back: CALL puts A, B and the first three pointers back the ; way it found them. SETD.0 0x80 0x00 DPUP.0 0d08 LDA.0 SETD.1 LoadedEntry STA.1 INCD.0 INCD.1 LDA.0 STA.1 INIA 0x01 SETD.0 LoadedOk STA.0 RET ; LoadStatus is still the zero it was started at. ; Which of the seven happened, and where the words for it are. The two are set together so ; that no caller has to know both, and so that adding a way to fail cannot leave one of ; them behind. loadNoDisk: INIA 0d1 SETD.0 NoDisk BRI loadRefuse loadMissing: INIA 0d2 SETD.0 NoSuchFile BRI loadRefuse loadUnreadable: INIA 0d3 SETD.0 Unreadable BRI loadRefuse loadNotProgram: INIA 0d4 SETD.0 NotProgram BRI loadRefuse loadWrongVersion: INIA 0d5 SETD.0 WrongVersion BRI loadRefuse loadTooManyVectors: INIA 0d6 SETD.0 TooManyVectors BRI loadRefuse loadIsDirectory: INIA 0d7 SETD.0 IsDirectory loadRefuse: SETD.1 LoadMessage STD.0.1 SETD.1 LoadStatus STA.1 RET ; ---- Writing a file a block at a time ---- ; ; Three handlers over the three routines in sbfs.asm, and thin, because everything that is ; difficult about writing safely is down there where it is written once. ; ; DP0 names the file, DP3 is how many whole blocks and A is what is left over in the last ; one - the size said the way an entry says it, which is what lets this reach past the ; 65,535 bytes osFileSave can describe. handleFileStart: SETD.2 DiskReady PSHA LDA.2 BRA fileStartNoDisk POPA ; The tail is in A and the block count is in DP3, so the count has to come out of the ; pointer before anything else wants it. SETD.2 SbfsFileTail STA.2 ; The low byte comes off the Stack first, which is the same way round Type and every ; other reader takes a count out of DP3. Popping them the other way gave a block count ; of the size times two hundred and fifty six, and a start that could find no room. PSHD.3 POPB POPA SETD.2 SbfsFileBlocks STA.2 INCD.2 STB.2 ; What a name means on the disk is about to change, so the remembered file goes. CALL fileForget CALL sbfsStreamStart SRET fileStartNoDisk: POPA INIA 0d1 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; DP1 is where the block comes from, and A and B together say which block of the file it ; is, counting from zero - the same way osFileBlock is told which one to fetch. handleFileWrite: SETD.2 SbfsIndex STA.2 INCD.2 STB.2 CALL sbfsStreamWrite SRET ; DP1 is where the block goes, and A and B together say which one, the same way writing is ; told. Reads back a block of the file being written. handleFileFetch: SETD.2 SbfsIndex STA.2 INCD.2 STB.2 CALL sbfsStreamFetch SRET ; DP3 is how many whole blocks it came to and A is what is left over, told the same way ; osFileStart is told. It need not be what was asked for: a writer that cannot know its ; size until the last byte asks for enough at the start, where running out costs nothing, ; and says the truth here. The blocks it did not use go back. ; ; This is the only step that can lose anything, and the last one. handleFileDone: SETD.2 SbfsFileTail STA.2 PSHD.3 POPB POPA SETD.2 SbfsFileBlocks STA.2 INCD.2 STB.2 CALL fileForget CALL sbfsStreamDone SRET ; ---- osChangeDir ---- ; ; The service behind the shell's cd, and the reason the shell bothers to put the working ; directory back when a program stops: without something a program can call, that promise ; would have been about a thing that could not happen. ; ; DP0 names a directory. Q is zero if the machine is now in it. handleChangeDir: ; DP0 arrives holding the path, because an interrupt frame keeps the caller's registers ; and this runs with them still in place. What it hands BACK has to be written into the ; frame, since RETI puts every register back the way the caller had it - which is why Q ; is stored at DP2 plus two below rather than simply being set. SETD.2 DiskReady LDA.2 BRA changeDirNo CALL sbfsWalk BNQ changeDirNo ; The root has no entry to ask about, and is always somewhere that can be stood in. SETD.2 SbfsAt LDA.2 INCD.2 LDB.2 OR BRQ changeDirTake SETD.2 SbfsFoundFlags LDA.2 INIB 0x02 AND BRQ changeDirNo changeDirTake: CALL fileForget ; A relative path means something else from here. SETD.0 SbfsAt SETD.1 SbfsCwd CALL sbfsCopyWord RSTA RSTB CCF ADD ; A is the answer, so Q becomes it. SRET changeDirNo: INIA 0d1 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; ---- cd ---- ; ; Moves the machine. What changes is two bytes, because the working directory is an entry ; index and nothing else: no path is stored anywhere, and the one on the prompt is worked ; out again each time from the chain of parents. ; ; "cd" on its own goes to the root, which is the only place that is always there and the ; only sensible thing to mean by home on a machine with no idea who is using it. doCd: SETD.0 DiskReady LDA.0 BRA fileNoDisk SETD.1 TextRest LDD.0.1 LDA.0 BRA cdRoot CALL sbfsWalk BNQ cdNoSuch ; It has to be a directory to stand in. Ending at the root is fine and is the one case ; with no entry to ask, since the root is not an entry. SETD.0 SbfsAt LDA.0 INCD.0 LDB.0 OR BRQ cdTake SETD.0 SbfsFoundFlags LDA.0 INIB 0x02 AND BRQ cdNotDirectory cdTake: ; WHAT A RELATIVE PATH MEANS HAS JUST CHANGED, so the remembered file goes. It is keyed ; on the path as somebody typed it, and "notes.txt" is a different file from here than ; it was a moment ago. Nothing about the entry it remembers has changed, which is what ; makes this the kind of stale that is believed rather than noticed. CALL fileForget SETD.0 SbfsAt SETD.1 SbfsCwd CALL sbfsCopyWord BRI prompt cdRoot: CALL fileForget SETD.0 SbfsCwd RSTA STA.0 INCD.0 STA.0 BRI prompt cdNoSuch: SETD.0 NoSuchFile BRI fileComplain cdNotDirectory: SETD.0 NotDirectory BRI fileComplain ; ---- mkdir and rmdir ---- ; ; Two commands rather than one that works out what you meant, and delete stays for files ; only. Each of the four says exactly what it will take, so none of them can be the one ; that took away more than was asked for. doMkdir: SETD.0 DiskReady LDA.0 BRA fileNoDisk SETD.1 TextRest LDD.0.1 LDA.0 BRA mkdirWhat CALL sbfsMakeDir BNQ mkdirFailed SETD.0 MadeText CALL printString CALL newLine BRI prompt mkdirWhat: SETD.0 MkdirWhat BRI fileComplain mkdirFailed: SETD.0 MkdirNo BRI fileComplain doRmdir: SETD.0 DiskReady LDA.0 BRA fileNoDisk SETD.1 TextRest LDD.0.1 LDA.0 BRA rmdirWhat ; What a name means on the disk is about to change, so the remembered file goes. CALL fileForget CALL sbfsRemoveDir BNQ rmdirFailed SETD.0 RemovedText CALL printString CALL newLine BRI prompt rmdirWhat: SETD.0 RmdirWhat BRI fileComplain rmdirFailed: SETD.0 RmdirNo BRI fileComplain ; ---- Writing out where the machine is ---- ; ; Builds the working directory's path into CwdText and leaves CwdAt pointing at where it ; begins. Nothing stores a path, so this walks up the chain of parents. ; ; WRITTEN BACKWARDS, from the end of the buffer towards the front, because that is the ; order the names arrive in and reversing them afterwards would want somewhere to put them ; in the meantime. What comes back is a pointer into the middle of the buffer rather than ; to the front of it, which costs nothing to print from. shellPath: ; The end of the buffer, holding the zero that ends the string. SETD.0 CwdText DPUP.0 0d126 RSTA STA.0 SETD.1 CwdAt STD.0.1 ; HOW MUCH THERE IS TO WRITE INTO, COUNTED DOWN. Nothing bounds how deep the directories ; go: a path is capped at what one operation can name, but "mkdir a" and "cd a" are each ; well inside that and can be typed all day. This walk writes BACKWARDS from the end, so ; running out means walking off the FRONT of the buffer and into whatever the assembler ; put below it - which was the shell's own command names. Six directories of twenty two ; characters was enough, and the first five bytes to go were the word "exit", so the ; shell stopped recognising the command for leaving. ; ; A hundred and twenty three of the hundred and twenty six, the other three being kept ; back for the dots that say it was cut. INIA 0d123 SETD.0 CwdRoom STA.0 RSTA SETD.0 CwdCut STA.0 ; Where the walk up starts. SETD.0 SbfsCwd SETD.1 CwdWalk CALL sbfsCopyWord shellPathStep: SETD.0 CwdWalk LDA.0 INCD.0 LDB.0 OR BRQ shellPathDone ; The root, which is where every path begins. SETD.0 CwdWalk SETD.1 SbfsTarget CALL sbfsCopyWord SETD.0 SbfsTarget CALL sbfsBackWord CALL sbfsAtIndex BNQ shellPathDone ; The disk would not read, so say as much as is known. ; The name goes in front of what is there, and a separator in front of that. SETD.0 SbfsName CALL shellPathPrepend SETD.0 CwdCut LDA.0 BNA shellPathReady ; It would not all fit, so there is nothing above worth asking for. SETD.0 SbfsUpParent SETD.1 CwdWalk CALL sbfsCopyWord BRI shellPathStep shellPathDone: ; A machine at the root has written nothing at all, and the path to the root is the ; separator on its own. SETD.0 CwdAt LDD.1.0 LDA.1 BNA shellPathReady SETD.0 Separator CALL shellPathPrepend shellPathReady: RET ; DP0 names a string. Puts it in front of what CwdAt points at, with a separator before ; it, and moves CwdAt back over the lot. ; ; The string has to be measured before it can be written, since it is written from its ; last character backwards. Nothing here is long enough for that to be worth avoiding. shellPathPrepend: SETD.1 PathLength RSTA STA.1 pathMeasure: LDA.0 BRA pathMeasured SETD.1 PathLength LDA.1 INCA STA.1 INCD.0 BRI pathMeasure pathMeasured: ; DP0 is on the zero at the end. Step back onto the last character, unless there is not ; one, in which case only the separator goes in. SETD.1 CwdAt LDD.2.1 ; DP2 is where the string already begins. pathBack: SETD.1 PathLength LDA.1 BRA pathSeparator DECA STA.1 SETD.1 CwdRoom LDA.1 BRA pathCut ; The front of the buffer, and the next byte would be past it. DECA STA.1 DECD.0 DECD.2 LDA.0 STA.2 BRI pathBack pathSeparator: SETD.1 CwdRoom LDA.1 BRA pathCut DECA STA.1 DECD.2 INIA 0x2F STA.2 SETD.1 CwdAt STD.2.1 RET ; Stopping rather than writing past the front. The names go on backwards, so what is ; already down is the DEEP end of the path - which is the end worth showing: the prompt ; says where you are, and the last two directories say that better than the first two do. ; ; Three dots in front, out of the room that was never counted, so there is always ; somewhere to put them. No separator: the name they sit against brought its own, or was ; cut off half way, and "..." reads correctly against either. pathCut: INIA 0x2E DECD.2 STA.2 DECD.2 STA.2 DECD.2 STA.2 SETD.1 CwdAt STD.2.1 INIA 0x01 SETD.1 CwdCut STA.1 RET ; ---- delete and rename ---- ; ; The two things a disk needs that reading and writing do not provide, and the two that ; anything editing a document will want from the shell as well as from a program. Deleting ; frees an entry and its blocks; renaming changes twenty two bytes and moves nothing. doDelete: SETD.0 DiskReady LDA.0 BRA fileNoDisk SETD.1 TextRest LDD.0.1 LDA.0 BRA deleteWhat ; What a name means on the disk is about to change, so the remembered file goes. CALL fileForget CALL sbfsDelete BNQ deleteFailed SETD.0 Deleted CALL printString CALL newLine BRI prompt deleteWhat: SETD.0 DeleteWhat BRI fileComplain deleteFailed: ; Two refusals arrive here as one. Asking again costs a walk of the directory, which is ; nothing on a path nobody takes twice, and it is the difference between "you typed a ; name that is not there" and "that is a directory, and delete does not take those". SETD.1 TextRest LDD.0.1 CALL sbfsFind BNQ deleteNoSuch SETD.0 SbfsFoundFlags LDA.0 INIB 0x02 AND BNQ deleteIsDirectory deleteNoSuch: SETD.0 NoSuchFile BRI fileComplain deleteIsDirectory: SETD.0 IsDirectory BRI fileComplain doRename: SETD.0 DiskReady LDA.0 BRA fileNoDisk SETD.1 TextRest LDD.0.1 LDA.0 BRA renameWhat ; Two names, so the rest of the line is split again. textSplit writes a zero over the ; space it cuts at, so what was one string becomes two without anything being copied. SETD.1 TextRest LDD.0.1 SETD.1 RenameFrom STD.0.1 CALL textSplit SETD.1 TextRest LDD.1.1 LDA.1 BRA renameWhat ; Only one name was given, and this needs both. SETD.2 RenameFrom LDD.0.2 ; What a name means on the disk is about to change, so the remembered file goes. CALL fileForget CALL sbfsRename BNQ renameFailed SETD.0 Renamed CALL printString CALL newLine BRI prompt renameWhat: SETD.0 RenameWhat BRI fileComplain renameFailed: ; Either there is no such file or the new name is already taken. Which of the two is not ; worth another message: both mean the disk does not have room for that name to move. SETD.0 RenameNo BRI fileComplain fileNoDisk: SETD.0 NoDisk fileComplain: CALL printString CALL newLine BRI commandFailed ; ---- drive ---- ; ; Which disk the shell is standing on. With nothing after it, says which; with a number, goes ; there - and the working directory goes with it, because where you are on a disk is part of ; which disk you are on rather than something the shell keeps on the side. doDrive: SETD.1 TextRest LDD.0.1 LDA.0 BRA driveSay ; One digit. Anything else is not a drive number, and the machines this is imitating never ; had ten drives either. INIB 0d48 CCF SUB MVQA INIB 0d10 CCF SUB BNC driveNoSuch ; Ten or more, so it was not a digit at all. ; Is there such a drive on this machine? SETD.1 DriveWanted STA.1 INB 0x25 ; How many drives, straight into B: there is no move from A to it. LDA.1 CCF SUB ; Borrows when the wanted one is inside the count. BNC driveNoSuch ; And is there anything readable in it? A drive with no disk is a real drive and an empty ; one, so this is a different answer from "there is no such drive". CALL sbfsDriveBit MVQA SETD.1 SbfsMounted LDB.1 AND BRQ driveNotReadable SETD.1 DriveWanted LDA.1 CALL sbfsUse BRI prompt driveSay: INA 0x24 INIB 0d48 CCF ADD OUTQ 0x00 CALL newLine BRI prompt driveNoSuch: SETD.0 DriveNoSuch BRI fileComplain driveNotReadable: SETD.0 DriveNotReadable BRI fileComplain ; ---- clear ---- ; ; The console has done this since before there was a screen to do it on: writing 1 to the ; command port. On a terminal it is what a terminal does with it, and on the Voyager it is ; the screen going blank and the cursor going home. doClear: INIA 0x01 OUTA 0x05 BRI prompt ; ---- echo ---- ; ; Says the rest of the line and nothing else. Say.sbx has done this since before there were ; scripts, and is the wrong shape for one: it is a program, so it has to be found on the ; disk and loaded and started, it prefixes what it was told with "it says:", and the system ; prints "finished" after it. That is three lines of noise around one line of narration. ; ; A script telling you what it is doing is the ordinary case now, so it costs a command ; rather than a program. With nothing after it, a blank line - which is what anybody will ; expect and is worth having for spacing a long script out. doEcho: SETD.1 TextRest LDD.0.1 CALL printString CALL newLine BRI prompt ; ---- do ---- ; ; Runs the lines in a file as though they had been typed. What makes a file one of these is ; the #! on the front of it, not its name and not a flag in its directory entry: the rule is ; that the entry holds only what the content cannot say about itself, and a script can say ; what it is. The loader already refuses anything that is not SBEX, so the two kinds of ; runnable thing turn each other away without either of them knowing about the other. doScript: SETD.1 TextRest LDD.0.1 LDA.0 BRA scriptNoName CALL scriptOpen BRQ prompt ; It is open, and the next line read will come from it. ; Which of the two went wrong. A number would be no use to anybody here. MVQA INIB 0x01 CCF SUB BRQ scriptNoFile INIB 0x02 CCF SUB BRQ scriptNotOne SETD.0 ScriptTooDeep BRI fileComplain scriptNotOne: SETD.0 ScriptNotOne BRI fileComplain scriptNoFile: SETD.0 ScriptNoFile BRI fileComplain scriptNoName: SETD.0 ScriptUsage BRI fileComplain ; Where the person was standing when a program was started, so it can be given back when the ; program has finished with the machine. A program that copies between two disks moves the ; drive as its own paths need it to; that is its business, and being left on the disk it ; happened to finish with is not what the person asked for. ; ; SET WHEREVER A PROGRAM STARTS, of which there are two: run, and typing a program's name. ; Restored in handleExit, which is the one place they both come back through. ; ---- Putting the screen somewhere and getting it back ---- ; ; A program that takes the whole screen leaves the shell a blank one, and everything that was ; on it - the listing you were reading, the error you were about to act on - is gone. There is ; nowhere to put 32K on a machine with 64K of Data Memory that CosmOS is already living in. ; ; A DRIVE MADE OF MEMORY IS SOMEWHERE. The map goes to a file on the scratch drive like any ; other file, and comes back the same way; the filesystem does the allocating, and this had to ; invent nothing at all to have somewhere to put it. ; ; EVERYTHING A PROGRAM CAN DISTURB, which is more than the part on screen. The map's off ; screen rows are the console's scrollback; the tiles are the font, which a program that ; redefines one has overwritten; and the palette is where the console's own colours live - ; Grid could give back the map and not the colours, and handed the shell green text on blue. ; ; 192 pages of tiles and map, which are next to each other, then the four of palette. 196 in ; all, and a register block on the front. ; ; ---- Saved on being asked, restored on the way out ---- ; ; Saving on every program start would be cheap enough. Restoring on every exit would be ; WRONG: dir, Files and Say print and stop, and their output is the reason you ran them. ; So a program says it is taking the screen, and one that says nothing behaves exactly as ; every program did before this existed. screenTake: SETD.0 SbfsScratch1 LDA.0 INIB 0xFF CCF SUB BRQ screenNoScratch ; No volatile drive, so nowhere to put it. ; Where the caller was, and where the file is going. INA 0x24 SETD.0 ScreenWasDrive STA.0 SETD.0 SbfsScratch1 LDA.0 CALL sbfsUse CALL screenBank ; A hundred and twenty nine blocks: one of registers, then the map. SETD.0 SbfsFileBlocks RSTA STA.0 INCD.0 INIA 0d197 STA.0 SETD.0 SbfsFileTail RSTA STA.0 SETD.0 ScreenFileName CALL sbfsStreamStart BNQ screenTakeFailed ; Block nought is where the screen was, rather than what was on it: the cursor, the four ; scroll registers and the mode. A picture put back under a different origin is not the ; picture that was taken. CALL screenClearBlock SETD.0 ScreenBlock INA 0x03 STA.0 INCD.0 INA 0x04 STA.0 INCD.0 INA 0x34 STA.0 INCD.0 INA 0x36 STA.0 INCD.0 INA 0x37 STA.0 INCD.0 INA 0x38 STA.0 INCD.0 INA 0x31 STA.0 RSTA RSTB SETD.2 SbfsIndex STA.2 INCD.2 STB.2 SETD.1 ScreenBlock CALL sbfsStreamWrite BNQ screenTakeFailed ; And the map, a block at a time through the one buffer there is. RSTA SETD.0 ScreenAt STA.0 screenTakeBlock: SETD.0 ScreenAt LDA.0 CALL screenPageFor MVQA CALL screenFromVideo SETD.0 ScreenAt LDA.0 INCA SETD.2 SbfsIndex RSTB STB.2 INCD.2 STA.2 ; Block n of the map is block n+1 of the file. SETD.1 ScreenBlock CALL sbfsStreamWrite BNQ screenTakeFailed SETD.0 ScreenAt LDA.0 INCA STA.0 INIB 0d196 CCF SUB BNQ screenTakeBlock SETD.0 SbfsFileBlocks RSTA STA.0 INCD.0 INIA 0d197 STA.0 SETD.0 SbfsFileTail RSTA STA.0 CALL sbfsStreamDone BNQ screenTakeFailed INIA 0x01 SETD.0 ScreenSaved STA.0 CALL screenGoBack RSTA RSTB CCF ADD RET screenTakeFailed: RSTA SETD.0 ScreenSaved STA.0 CALL screenGoBack screenNoScratch: RSTA INIB 0d1 CCF ADD RET ; Whatever screenTake put away, put back. Nothing at all if it never ran. screenGive: SETD.0 ScreenSaved LDA.0 BRA screenGiveNone RSTA STA.0 ; Once only: the next program takes its own. INA 0x24 SETD.0 ScreenWasDrive STA.0 SETD.0 SbfsScratch1 LDA.0 CALL sbfsUse CALL screenBank SETD.0 ScreenFileName CALL fileLookup BNQ screenGiveDone ; The map first, then the registers, so that nothing is drawn under an origin that is about ; to change. RSTA SETD.0 ScreenAt STA.0 screenGiveBlock: SETD.0 ScreenAt LDA.0 INCA SETD.2 SbfsIndex RSTB STB.2 INCD.2 STA.2 SETD.1 ScreenBlock CALL sbfsReadOne BNQ screenGiveDone SETD.0 ScreenAt LDA.0 CALL screenPageFor MVQA CALL screenToVideo SETD.0 ScreenAt LDA.0 INCA STA.0 INIB 0d196 CCF SUB BNQ screenGiveBlock ; And where it was. RSTA RSTB SETD.2 SbfsIndex STA.2 INCD.2 STB.2 SETD.1 ScreenBlock CALL sbfsReadOne BNQ screenGiveDone SETD.0 ScreenBlock LDA.0 OUTA 0x03 INCD.0 LDA.0 OUTA 0x04 INCD.0 LDA.0 OUTA 0x34 INCD.0 LDA.0 OUTA 0x36 INCD.0 LDA.0 OUTA 0x37 INCD.0 LDA.0 OUTA 0x38 INCD.0 LDA.0 OUTA 0x31 screenGiveDone: CALL screenGoBack screenGiveNone: RET ; The registers a console cannot work without, put back. Not the picture - that is ; screenGive's, and only happens for a program that asked. screenSane: RSTA OUTA 0x37 OUTA 0x38 ; No fraction of a cell in either direction. ; ---- And glyphs and colours that can be read ---- ; ; A program may redefine any tile and any colour, and the font lives in tiles like anything ; else - so a program that redefined a letter has left the shell unable to spell, and one ; that wrote its own palette handed back green text on blue. Both used to be permanent: ; there was nowhere to get the originals from, because the only copy was the one that had ; been drawn over. The device has a character generator now and this is what it is for. ; ; BEFORE screenGive, like everything else here, so that a program which SAVED the screen ; gets back what was actually on it rather than what a clean machine looks like. This is ; the floor for the programs that saved nothing. INIA 0x03 OUTA 0x39 RET ; Video memory as bank 4, which is what makes it reachable at all. Bank 3 is the disk's; see ; the table in the CosmOS README, which exists because a program once took 3. screenBank: INIA 0d4 OUTA 0xE3 INIA 0x30 OUTA 0xE2 INIA 0x03 OUTA 0xE8 RET ; Back to the drive whoever called was standing on. screenGoBack: SETD.0 ScreenWasDrive LDA.0 CALL sbfsUse RET ; ---- Which page of video memory a saved block is ---- ; ; Nought to 191 are the tiles and the map, which sit next to each other from 0x0000. After ; that comes 16K of nothing, so 192 to 195 jump to 0xFC and are the palette. One sum rather ; than two loops, because two loops is two places to get the file's block numbers wrong. screenPageFor: INIB 0d192 CCF SUB BRC screenPageDirect ; Borrowed, so it is below 192 and the page is the index. MVQA INIB 0xFC CCF ADD RET screenPageDirect: RSTB CCF ADD RET ; A holds a page of video memory. Its 256 bytes come into ScreenBlock. screenFromVideo: PSHA INIA 0d4 OUTA 0xE0 POPA OUTA 0xE1 RSTA OUTA 0xE2 INIA 0x01 OUTA 0xE3 ; Into Data Memory. CALL screenBufferDest CALL screenLength INIA 0x01 OUTA 0xE8 RET ; A holds a page of video memory. ScreenBlock goes back into it. screenToVideo: PSHA INIA 0d4 OUTA 0xE3 POPA OUTA 0xE4 RSTA OUTA 0xE5 INIA 0x01 OUTA 0xE0 ; Out of Data Memory. CALL screenBufferSource CALL screenLength INIA 0x01 OUTA 0xE8 RET ; Where ScreenBlock is, told to the controller. A Data Pointer's two bytes cannot be read out ; of it, so it goes to memory first and comes back a byte at a time. screenBufferDest: SETD.1 ScreenBlockAt SETD.0 ScreenBlock STD.0.1 LDA.1 OUTA 0xE4 INCD.1 LDA.1 OUTA 0xE5 RET screenBufferSource: SETD.1 ScreenBlockAt SETD.0 ScreenBlock STD.0.1 LDA.1 OUTA 0xE1 INCD.1 LDA.1 OUTA 0xE2 RET screenLength: INIA 0x01 OUTA 0xE6 RSTA OUTA 0xE7 ; 0x0100, which is one block. RET ; The block, emptied. Everything the register block below does not fill has to be nought, or ; a screen restored would carry whatever the last file read left in here. screenClearBlock: SETD.0 ScreenBlock RSTB screenClearByte: RSTA STA.0 INCD.0 DECB BNB screenClearByte RET ; ---- run ---- ; ; Hands the machine to whatever was loaded. Where the Stack is now is written down first, ; because the program is not going to unwind anything it pushes and the exit handler has ; to be able to put the Stack back. doRun: SETD.0 LoadedOk LDA.0 BRA runNothing ; Where typing a program's name arrives, having loaded it on the way. The argument comes ; out of TextRest either way: after "run" that is what followed the word, and after a ; program's own name it is what followed the name, which is the same thing meaning the ; same thing. runLoaded: INA 0x24 SETD.0 RunDrive STA.0 MVSD.0 SETD.1 SystemStack STD.0.1 ; And where the machine is, so that the exit handler has something to put back. SETD.0 SbfsCwd SETD.1 SavedCwd CALL sbfsCopyWord ; Whatever followed the word "run" is kept where the program can ask for it. Copied ; rather than pointed at, because what it is pointing at is the line the shell typed ; into, and a program is entitled to outlive the shell's opinion of that. SETD.1 TextRest LDD.0.1 SETD.1 RunArgument INIB 0d64 CALL copyText CALL installVectors ; The entry address is a number until BRD makes it a place. DP3 is the one to build it ; in, because it is the pointer nothing puts back. SETD.1 LoadedEntry LDD.3.1 BRD.3 runNothing: SETD.0 NothingLoaded CALL printString CALL newLine BRI commandFailed ; DP0 is a string, DP1 is where it should go, and B is how much room there is counting ; the zero on the end. What does not fit is left behind, and what is written is a string ; either way. copyText: BRB copyTextDone ; No room at all, so nothing is written, not even the zero. copyTextLoop: DECB BRB copyTextEnd ; Only room for the terminator now. LDA.0 STA.1 BRA copyTextDone INCD.0 INCD.1 BRI copyTextLoop copyTextEnd: RSTA STA.1 copyTextDone: RET ; ---- Putting a program's vectors in, and taking them out again ---- ; ; The vector table lives in Program Memory, which no instruction can write, so both of ; these go through the memory controller. Port 0xE9 reads a byte from the source and writes ; a byte to the destination, stepping the address on either way, so a two byte entry is two ; reads or two writes and no address arithmetic in between. ; ; What was in the slot is kept before anything replaces it, and put back afterwards, rather ; than the slot being cleared. Clearing would be wrong wherever a program has installed a ; handler over one the system was already using: the program is allowed to do that, and ; when it goes, what it covered up has to come back rather than becoming a hole. installVectors: SETD.0 LoadedVectorCount LDA.0 BRA installDone SETD.1 VectorsLeft STA.1 SETD.3 LoadedVectors installOne: ; DP3 walks one six byte entry: where it goes, what goes there, and room for what was ; there before. Reading and writing the same slot, so the controller is pointed at it ; from both ends at once and the address is only worked out once. RSTA OUTA 0xE0 ; SourceBank: Program Memory. OUTA 0xE3 ; DestBank: the same. LDA.3 OUTA 0xE1 OUTA 0xE4 INCD.3 LDA.3 OUTA 0xE2 OUTA 0xE5 INCD.3 ; On the handler. ; What is there now, before anything replaces it. INA 0xE9 PSHA INA 0xE9 PSHA ; And the handler in its place. LDA.3 OUTA 0xE9 INCD.3 LDA.3 OUTA 0xE9 INCD.3 ; On the two bytes kept for what was there before. ; The Stack gives them back in the reverse of the order they went on, so the low byte ; arrives first and is written to the second of the two. Getting this the natural way ; round instead put the low byte where the high one goes and the high byte over the ; handler, which the first run of a program survives - the table is already written by ; then - and the second run does not. POPA INCD.3 STA.3 DECD.3 POPA STA.3 INCD.3 INCD.3 SETD.1 VectorsLeft LDA.1 DECA STA.1 BNA installOne installDone: RET removeVectors: SETD.0 LoadedVectorCount LDA.0 BRA removeDone SETD.1 VectorsLeft STA.1 SETD.3 LoadedVectors removeOne: RSTA OUTA 0xE3 ; DestBank: Program Memory. LDA.3 OUTA 0xE4 INCD.3 LDA.3 OUTA 0xE5 INCD.3 INCD.3 INCD.3 ; Past the handler, to what was underneath it. LDA.3 OUTA 0xE9 INCD.3 LDA.3 OUTA 0xE9 INCD.3 SETD.1 VectorsLeft LDA.1 DECA STA.1 BNA removeOne removeDone: RET ; ---- The services ---- ; ; These are what a loaded program is allowed to ask for. The names and their numbers come ; from services.asm, which the programs include as well, so neither side writes a number ; down and the two cannot disagree about them. ; ; A handler arrives with the caller's registers exactly as they were: an interrupt frame ; is pushed, not cleared. So the pointer a program put in DP0 is still there to be used. ; The disk finishing, acknowledged and ignored. ; ; The system drives the disk by asking its status port and waiting, so it has no use for ; the line. But the disk raises one after every operation whether anybody wants it or not, ; and a line goes on waiting while the Interrupt Flag is down rather than being lost. The ; shell keeps the flag down, so the line from the last disk read was still standing when ; the first program to enable interrupts ran, and it arrived there - a fault, in a program ; that had never heard of the disk, blamed on the innocent instruction that let it through. ; ; Answering a line is what takes it down, so this is one instruction and that is the point. diskDone: RETI handlePrintString: CALL printString RETI handleReadLine: CALL editLinePlain ; readLine works out how long the line was, and it is already in Q where readLine left ; it. SRET keeps Q and DP3 and puts everything else back, so the answer simply stands. SRET ; What the program was asked to work on. DP0 says where to put it and B how much room ; there is, counting the zero on the end, which is the same bargain readLine offers. ; ; Being asked for rather than left at an agreed address is deliberate. The two sides of ; this already have to agree on a vector number and nothing else, and that number is ; written down once in services.asm; an address would be a second thing to agree about, in ; a memory map that is a convention rather than anything enforced. handleArgument: PSHD.0 POPD.1 SETD.0 RunArgument 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 ; A two byte answer, and DP3 is where a routine hands one back. Built with the ; Stack rather than written into the frame: SRET leaves DP3 alone, so there is ; nothing to reach into. PSHA then PSHB puts the high byte above the low one, ; which is the order POPD reads them in. PSHA PSHB POPD.3 RSTA RSTB CCF ADD ; Q is zero: it is there. SRET fileReadNo: INIA 0d1 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; 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 ; What a name means on the disk is about to change, so the remembered file goes. CALL fileForget CALL sbfsSaveFile SRET fileSaveNoDisk: POPA INIA 0d1 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; DP0 names it. Q is zero if it went. handleFileDelete: SETD.2 DiskReady LDA.2 BRA serviceNoDisk ; What a name means on the disk is about to change, so the remembered file goes. CALL fileForget CALL sbfsDelete SRET ; 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 ; What a name means on the disk is about to change, so the remembered file goes. CALL fileForget CALL sbfsRename SRET serviceNoDisk: INIA 0d1 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; ---- Reading a file that will not fit ---- ; ; A file bigger than Data Memory cannot be handed over whole, and CosmOS's own source is ; now that file, so these two are how anything reads one: ask how many blocks, then ask for ; each block in turn. There is no open and no close. A program that stops halfway through ; leaves nothing behind, because there was never anything to leave. ; Finds a file, or remembers that it already did. DP0 names it. Q is zero if it is there, ; and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it exactly the way ; sbfsFind leaves them - whether the search happened or not, which is the whole point. fileLookup: SETD.2 FileCacheValid LDA.2 BRA fileLookupSearch SETD.1 FileCacheName CALL textSame BNQ fileLookupSearch ; ---- The same file as last time, and possibly not the same disk ---- ; ; The description has to be put back, because anything that went to the disk in between - a ; directory listing, a program being loaded - left its own answer in those three. ; ; AND THE DRIVE WITH IT. Skipping the walk skips the drive the path named, so a cache hit ; on a machine that had moved read the right block numbers off the wrong disk. Copying ; between two disks is exactly that: block 0 walks and goes to the source, the write goes to ; the destination, and block 1 hits this cache. It only showed on files of more than one ; block, because a file of one is never looked up twice. SETD.0 FileCacheDrive LDA.0 CALL sbfsUse SETD.0 SbfsFileStart SETD.2 FileCacheStart CALL sbfsSetWord SETD.0 SbfsFileBlocks SETD.2 FileCacheBlocks CALL sbfsSetWord SETD.0 FileCacheTail LDA.0 SETD.0 SbfsFileTail STA.0 RSTA RSTB CCF ADD ; Q is zero: found. RET fileLookupSearch: CALL sbfsFind BNQ fileLookupMissing ; Remember it. DP0 still names the file: a CALL puts the pointers back, which is the one ; place that convention is a convenience rather than an obstacle. ; ; A WHOLE PATH IS KEPT, not the twenty two bytes a name has. Keeping twenty two of a ; longer path cannot hand back the wrong file - textSame wants both strings to end in ; the same place, so a cut down entry misses rather than matching something else - but ; it can never match either, so every path longer than a name would go to the disk every ; single time and the cache would quietly stop being one. SETD.1 FileCacheName INIB 0d64 CALL copyText SETD.0 FileCacheStart SETD.2 SbfsFileStart CALL sbfsSetWord SETD.0 FileCacheBlocks SETD.2 SbfsFileBlocks CALL sbfsSetWord SETD.0 SbfsFileTail LDA.0 SETD.0 FileCacheTail STA.0 ; ---- And which disk those block numbers are on ---- ; ; A start block means nothing without it. The walk above went to whatever drive the path ; named, so the answer is the drive now. INA 0x24 SETD.0 FileCacheDrive STA.0 ; Marked good last, so that a cache half filled is never a cache believed. INIA 0d1 SETD.0 FileCacheValid STA.0 RSTA RSTB CCF ADD ; Q is zero: found. RET fileLookupMissing: RET ; Q is not zero, and sbfsFind is what made it so. ; Throws the remembered file away. Everything that can change what a name means on the disk ; calls this before it does: a file saved over may have moved, a deleted one is gone, and a ; renamed one answers to something else. A remembered start block that survived any of ; those is a pointer at whatever took its place. ; ; Q is deliberately untouched, so this can be dropped into a handler without disturbing the ; answer that handler is in the middle of working out. fileForget: RSTA SETD.0 FileCacheValid STA.0 RET ; DP0 names it. Q is zero if it is there, and DP3 comes back holding how many blocks it ; occupies, counting a part one on the end. ; ; Blocks, not bytes, and that is forced rather than chosen: a file on a sixteen megabyte ; disk is up to twenty four bits long, which does not fit in a pointer. Blocks do, and the ; bytes in the last one come back from osFileBlock when the reader gets there. handleFileInfo: SETD.2 DiskReady LDA.2 BRA fileInfoNoDisk CALL fileLookup BNQ fileInfoMissing CALL sbfsFileExtent SETD.2 SbfsWantBlocks LDA.2 INCD.2 LDB.2 ; A two byte answer, and DP3 is where a routine hands one back. Built with the ; Stack rather than written into the frame: SRET leaves DP3 alone, so there is ; nothing to reach into. PSHA then PSHB puts the high byte above the low one, ; which is the order POPD reads them in. PSHA PSHB POPD.3 RSTA RSTB CCF ADD ; Q is zero: it is there. SRET fileInfoNoDisk: INIA 0d1 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET fileInfoMissing: INIA 0d2 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; DP0 names it, DP1 says where to put it, and A and B together are which block, counting ; from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes ; belong to the file. handleFileBlock: ; Which block, before anything else, because finding out whether there is a disk needs A ; and there is nowhere else the number is written down. SETD.2 SbfsIndex STA.2 INCD.2 STB.2 SETD.2 DiskReady LDA.2 BRA fileBlockNoDisk CALL fileLookup BNQ fileBlockMissing ; Running off the end is how a reader finds out it has finished, so it gets an answer of ; its own rather than being told the disk failed. CALL sbfsFileExtent SETD.0 SbfsIndex SETD.2 SbfsWantBlocks CALL sbfsCompareWord BNC fileBlockPastEnd ; The Carry is set only when the index is the smaller. CALL sbfsReadOne ; DP1 still says where. A CALL puts the pointers back. BNQ fileBlockFailed ; How much of it is the file's. Every block but a short last one is a whole 256, and 256 ; is why this answers in a pointer instead of a register. SETD.2 SbfsFileTail LDA.2 BRA fileBlockWhole ; Nothing partial on the end, so they are all whole. SETD.0 SbfsIndex SETD.2 SbfsFileBlocks CALL sbfsCompareWord BNQ fileBlockWhole ; Not the last one. SETD.2 SbfsFileTail LDB.2 RSTA BRI fileBlockAnswer fileBlockWhole: INIA 0x01 RSTB fileBlockAnswer: ; A two byte answer, and DP3 is where a routine hands one back. Built with the ; Stack rather than written into the frame: SRET leaves DP3 alone, so there is ; nothing to reach into. PSHA then PSHB puts the high byte above the low one, ; which is the order POPD reads them in. PSHA PSHB POPD.3 RSTA RSTB CCF ADD ; Q is zero: it is there. SRET fileBlockNoDisk: INIA 0d1 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET fileBlockMissing: INIA 0d2 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET fileBlockPastEnd: INIA 0d3 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET fileBlockFailed: INIA 0d4 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; A breakpoint. Shows every register as the interrupted program had them, waits for a key, ; and returns as though nothing happened. ; ; EVERY VALUE COMES OUT OF THE FRAME, not out of the registers, because by the time this ; runs the registers belong to the handler. The frame is what the program had, and RETI is ; going to give it all back, so what is shown is what will be resumed with. ; ; DP3 holds the frame throughout. It survives a CALL, and console.asm promises not to ; disturb it, which is what lets the printing routines be used between one field and the ; next. The Stack Pointer comes back to the same place after a balanced call, so the frame ; stays where it was found. ; ; +1 Status +2 Q +3 A +4 B +5 DP3 +7 DP2 +9 DP1 +11 DP0 +13 where it resumes handleBreak: MVSD.3 ; Where it broke, which is two before where it resumes: the SWI and the vector it names. SETD.0 BreakText CALL printString PSHD.3 POPD.0 DPUP.0 0d13 LDA.0 PSHA ; The high half, while the low one is worked on. INCD.0 LDA.0 INIB 0d2 CCF SUB ; Two back from where it resumes: the SWI and the vector it names. MVQA POPB BRC breakBorrowed ; It borrowed, so the high half comes down by one. BRI breakAddress breakBorrowed: DECB breakAddress: PSHA ; low PSHB ; high POPA CALL printByteHex POPA CALL printByteHex CALL newLine SETD.0 ARegText PSHD.3 POPD.1 DPUP.1 0d3 CALL breakByte SETD.0 BRegText PSHD.3 POPD.1 DPUP.1 0d4 CALL breakByte SETD.0 QRegText PSHD.3 POPD.1 DPUP.1 0d2 CALL breakByte SETD.0 SRegText PSHD.3 POPD.1 INCD.1 CALL breakByte ; And which bits of it those are, since a debug dump that makes you look the number up is ; only half of one. Halt is not among them: the machine is plainly not halted. PSHD.3 POPD.1 INCD.1 LDA.1 PSHA INIB 0x01 AND BRQ breakNoCarry SETD.0 CarryText CALL printString breakNoCarry: POPA PSHA INIB 0x02 AND BRQ breakNoFault SETD.0 FaultText CALL printString breakNoFault: POPA INIB 0x04 AND BRQ breakNoInts SETD.0 IntsText CALL printString breakNoInts: CALL newLine SETD.0 DP0Text PSHD.3 POPD.1 DPUP.1 0d11 CALL breakWord SETD.0 DP1Text PSHD.3 POPD.1 DPUP.1 0d9 CALL breakWord SETD.0 DP2Text PSHD.3 POPD.1 DPUP.1 0d7 CALL breakWord SETD.0 DP3Text PSHD.3 POPD.1 DPUP.1 0d5 CALL breakWord ; The Stack Pointer is not in the frame, because the frame is where the Stack Pointer is. ; What the program had is fourteen bytes above this one, that being what entering an ; interrupt puts down. SETD.0 SPText CALL printString PSHD.3 POPD.0 DPUP.0 0d14 PSHD.0 POPB POPA CALL printByteHex PSHB POPA CALL printByteHex CALL newLine ; Anything typed carries on. Reading the data port waits however the console is set, which ; is one key if the program asked for key mode and a whole line if it did not - and either ; way it is the program's own console being borrowed for a moment. SETD.0 ResumeText CALL printString INA 0x00 CALL newLine RETI ; DP0 names a field and DP1 points at it in the frame. The caller does the stepping, with ; DPUP and a number written into the program, because a routine cannot hand a pointer back: ; CALL saves DP0 to DP2 and RET puts them back, so a walk done in here would be undone on ; the way out. Written that way first, and every field showed the frame's first byte. breakByte: CALL printString LDA.1 CALL printByteHex INIA 0x20 OUTA 0x00 RET ; The same for the two byte fields, most significant first the way the frame holds them. breakWord: CALL printString LDA.1 CALL printByteHex INCD.1 LDA.1 CALL printByteHex INIA 0x20 OUTA 0x00 RET ; 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. ; ; Which is exactly why this cannot RETI. Its return address is on the Stack it just walked ; away from, so it branches to the prompt instead. ; What the last program made of what it was asked to do. Kept rather than shown: a program ; that failed has already said so in words, and a number beside that would be noise. It is ; here for the thing that cannot read words - whatever comes to run programs in sequence and ; has to decide whether to run the next one. ; ; IT IS NOT LineFailed, and the difference is worth keeping. This one is a PROGRAM'S answer ; and belongs to whoever asks for it, which the shell's own status command does and a test ; records. Zeroing it as each line began - which is what the script reader wanted - wiped ; the answer before the command that reports it could read its own line. Two questions, two ; bytes. handleLastStatus: SETD.2 LastStatus LDA.2 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; ---- How the last start went, and settling it ---- ; ; The answer goes in Q by writing into this handler's own frame, which is how every service ; here hands anything back: a handler arrives with the caller's registers pushed, not ; cleared, and RETI puts them back - so the way to return a value is to change the copy the ; return is going to restore. handleBootState: SETD.2 DiskReady LDA.2 BRA bootStateNone CALL sbfsBootState BNQ bootStateNone SETD.2 SbfsStateWas LDA.2 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET bootStateNone: ; No disk, or one that would not answer. Nothing there to be unsettled about. RSTA RSTB CCF ADD ; A is the answer, so Q becomes it. SRET handleBootSettle: SETD.2 DiskReady LDA.2 BRA bootSettleNo RSTA CALL sbfsSetBootState BNQ bootSettleNo RSTA RSTB CCF ADD ; A is the answer, so Q becomes it. SRET bootSettleNo: INIA 0d1 RSTB CCF ADD ; A is the answer, so Q becomes it. SRET ; The program is about to draw over everything. Q says whether what is there now will come ; back, and a program that is told no carries on anyway. handleTakeScreen: CALL screenTake MVQA MVSD.2 DPUP.2 0d02 STA.2 RETI handleExit: ; ---- What the program made of it ---- ; ; A, before anything below disturbs it. IN A RATHER THAN Q, and that is not a departure ; from the rule that a service answers in Q: this one takes an argument, the way ; osPrintNumber takes A and B, and it never returns to answer anything. The register a ; subroutine cannot hand anything back in is exactly the one that is free here - and Q is ; the ALU's output, so setting it to a small number costs four instructions where A costs ; one. SETD.1 LastStatus STA.1 ; ---- The screen made usable, whether or not the picture can be given back ---- ; ; A fraction of a cell is never what anybody wants left behind: the console draws in whole ; cells, so a view three pixels into one puts every character three pixels out for ever. ; That is true whether or not there was anywhere to save the picture, so it is not part of ; the saving - a program refused by osTakeScreen still must not leave the shell squinting. ; ; Before screenGive, so that a restored screen's own registers win. CALL screenSane ; What was on the screen before this program had it, if it asked for that. CALL screenGive ; The drive the person was on, whatever the program did with it. PSHA SETD.1 RunDrive LDA.1 CALL sbfsUse POPA ; And the line that started it failed, if the program says it did. The script reader asks ; one question - did this line work - and a program answering "no" is one of the ways it ; can be answered. BRA exitWorked INIA 0x01 SETD.1 LineFailed STA.1 exitWorked: SETD.1 SystemStack LDD.0.1 MVDS.0 ; Whatever the program put in the vector table comes out again. A vector points into the ; program that supplied it, and the program is gone, so anything left installed would aim ; an interrupt at whatever those addresses hold next. CALL removeVectors ; And where the machine was before the program had it, for the same reason and by the ; same discipline as the Stack above and the vectors just now: a program is entitled to ; move about, and the shell is entitled to find itself where it left off. What a relative ; path means has changed back, so the remembered file goes with it. SETD.0 SavedCwd SETD.1 SbfsCwd CALL sbfsCopyWord CALL fileForget ; The console goes back to how the shell wants it, whatever the program left it in: line ; mode, and not interrupting. A program that wanted either is expected to put it back ; itself, but one that stopped early, or forgot, would otherwise hand back a shell with ; no echo and no backspace, or one being interrupted about keys it is reading anyway. ; This undoes everything the control port can be asked for and puts the cursor back, and ; asking for what is already the case costs a byte out of a port and does nothing. That ; is the right price for not having to know. ; ; THE CURSOR IS PUT BACK RATHER THAN LEFT, because a program that borrowed key mode and ; handed it back the way it was told to writes zero, which turns the cursor off. The shell ; owns the prompt, so the shell is what makes sure there is something blinking at it. INIA 0x04 OUTA 0x02 ; The ink too, and for the same reason as the cursor: a program that chose a colour is not ; around to put it back, and neither is one the fault screen printed for. The shell owns ; the prompt, so the shell is what makes sure it is readable. RSTA OUTA 0x06 ; ---- Finished, or stopped ---- ; ; A program that faulted did not finish, and saying so would be the shell's own word ; against what the fault screen just said in red immediately above it. SETD.1 FaultStopped LDA.1 BRA exitFinished RSTA STA.1 ; Cleared, so the next program is not blamed for this one. SETD.0 Stopped BRI exitSay exitFinished: SETD.0 Finished exitSay: CALL printString CALL newLine BRI prompt ; ---- dump ---- ; ; dump Sixty four more bytes, carrying on from the last one. ; dump From the start of that bank. ; dump From there. ; ; is program, data, or a bank number in hexadecimal. That the CPU cannot read ; Program Memory and this can is the whole point: the instruction set has no way to look ; at itself, and the controller does, so a monitor is possible at all only through it. ; 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 bankWhat SETD.1 ProgramWord CALL textSame BRQ bankProgram SETD.1 DataWord CALL textSame BRQ bankData CALL textHexWord BNQ bankWhat SETD.0 TextValue INCD.0 LDA.0 BRI bankSet bankProgram: RSTA BRI bankSet bankData: INIA 0d1 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 STA.0 ; 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 commandFailed bankWhat: SETD.0 BankUsage CALL printString CALL newLine BRI commandFailed ; 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 dumpGo ; Nothing said, so carry on from where the last one stopped. CALL textHexWord BNQ dumpBadWhere SETD.0 TextValue LDA.0 SETD.1 DumpAt STA.1 INCD.0 LDA.0 INCD.1 STA.1 dumpCheckBank: CALL bankPresent BRQ dumpNoBank dumpGo: SETD.0 ShowAsCode LDA.0 BNA disassembleGo INIA 0d4 SETD.0 DumpRows STA.0 dumpRow: SETD.0 DumpAt CALL printWordHex INIA 0d2 CALL printSpaces ; Point the controller at the row. Reading the Data port takes a byte and steps the ; source on, so the whole row is one instruction repeated. SETD.0 DumpBank LDA.0 OUTA 0xE0 SETD.0 DumpAt LDA.0 OUTA 0xE1 INCD.0 LDA.0 OUTA 0xE2 ; Sixteen bytes, kept as they go past so that they can be shown twice. INIA 0d16 SETD.0 DumpCount STA.0 SETD.1 DumpBytes dumpByte: INA 0xE9 STA.1 CALL printByteHex INIA 0x20 OUTA 0x00 INCD.1 SETD.0 DumpCount LDA.0 DECA STA.0 BNA dumpByte ; The same sixteen again, as characters. Anything that is not printable shows as a dot, ; because a control character sent to the console would move the cursor and ruin the ; shape of the dump. INIA 0x20 OUTA 0x00 INIA 0d16 SETD.0 DumpCount STA.0 SETD.1 DumpBytes dumpChar: LDA.1 INIB 0x20 CCF SUB BRC dumpDot ; Below a space. INIB 0x7F CCF SUB BNC dumpDot ; Delete, or above it. OUTA 0x00 BRI dumpCharNext dumpDot: INIA 0x2E OUTA 0x00 dumpCharNext: INCD.1 SETD.0 DumpCount LDA.0 DECA STA.0 BNA dumpChar CALL newLine ; Sixteen further along, carrying into the high byte if the low one wrapped. SETD.0 DumpAt INCD.0 LDA.0 INIB 0d16 CCF ADD STQ.0 BNC dumpRowNext SETD.0 DumpAt LDA.0 INCA STA.0 dumpRowNext: SETD.0 DumpRows LDA.0 DECA STA.0 BNA dumpRow BRI prompt 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 ExamineUsage CALL printString CALL newLine BRI commandFailed dumpNoBank: SETD.0 NoSuchBank CALL printString CALL newLine BRI commandFailed ; 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: ; A bank can be present and still refuse to be written: the controller's own table is ; published read only, and writing to it is refused. A refusal nobody catches stops the ; machine, which is a poor answer to somebody looking around with b and then typing s. CALL bankPresent SETD.0 BankFlags LDA.0 INIB 0x02 ; The read only bit of that bank's record. AND BNQ setReadOnly 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 setReadOnly: SETD.0 ReadOnlyText CALL printString CALL newLine BRI commandFailed setWhat: SETD.0 SetUsage CALL printString CALL newLine BRI commandFailed ; 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 commandFailed ; 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 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 ; ---- Assembling a line at a time ---- ; ; a
, then instructions until a line that is just a dot. The syntax is the ; assembler's - a selector rides on the mnemonic as LDA.0 or LDD.0.1, and leaving one off ; means Data Pointer 0, exactly as it does in a source file - so nothing learned here has to ; be unlearned when writing a real program. ; ; NUMBERS ARE HEXADECIMAL AND BARE. A source file writes 0x2000 or 0d16 because it has both ; and must say which; a monitor has only one and says so once, in the manual, rather than on ; every line. It is the same reason x and d take bare addresses. ; ; What cannot be written here is a label, and that is the whole difference between this and ; the assembler proper: a label is a promise to fill an address in later, and later is what ; a line at a time does not have. doAssemble: SETD.1 TextRest LDD.0.1 CALL textHexWord BNQ assembleWhat SETD.0 TextValue SETD.1 DumpAt CALL sbfsCopyWord ; Two bytes from DP0 to DP1, which sbfs already has. assembleLine: SETD.0 DumpAt CALL printWordHex SETD.0 AsmPrompt CALL printString SETD.0 AsmLine INIB 0d40 CALL shellReadRaw INA 0x01 INIB 0x02 ; ENDED, so there is nothing more to assemble. AND BNQ prompt SETD.0 AsmLine LDA.0 BRA assembleLine ; An empty line is somebody thinking. SETD.0 AsmLine SETD.1 DotText CALL textSame BRQ prompt SETD.0 AsmLine CALL textSplit ; The mnemonic, and whatever follows it. CALL assembleOne BRI assembleLine assembleWhat: SETD.0 AsmUsage CALL printString CALL newLine BRI commandFailed ; The mnemonic is in CommandLine's place - AsmLine - and TextRest is what followed it. ; Puts the bytes down and steps the cursor past them. assembleOne: CALL takeMnemonic CALL findByName BNQ assembleUnknown ; WHATEVER IT NEEDS IS READ BEFORE ANYTHING IS WRITTEN. Emitting the opcode first and ; discovering the missing value afterwards leaves half an instruction in memory, which the ; next line usually covers up and the last line of a session does not. SETD.0 AsmShape LDA.0 BRA assemblePut ; 0, nothing to read. DECA BRA assembleWantAddress ; 1 DECA BRA assembleWantByte ; 2 DECA BRA assemblePut ; 3, a selector and nothing else. DECA BRA assembleWantByte ; 4 DECA BRA assembleWantAddress ; 5 BRI assemblePut ; 6, two selectors. assembleWantByte: SETD.1 TextRest LDD.0.1 CALL textHexWord BNQ assembleNeedsValue BRI assemblePut assembleWantAddress: SETD.1 TextRest LDD.0.1 CALL textHexWord BNQ assembleNeedsValue assemblePut: ; Point the controller at the cursor. Every byte written steps it on by itself. SETD.0 DumpBank LDA.0 OUTA 0xE3 SETD.0 DumpAt LDA.0 OUTA 0xE4 INCD.0 LDA.0 OUTA 0xE5 SETD.0 AsmOpcode LDA.0 OUTA 0xE9 ; What follows depends only on the shape, exactly as it does when reading one back. SETD.0 AsmShape LDA.0 BRA assembleDone ; 0 DECA BRA assembleAddress ; 1 DECA BRA assembleByte ; 2 DECA BRA assembleSelector ; 3 DECA BRA assembleSelByte ; 4 DECA BRA assembleSelAddress ; 5 SETD.0 AsmSelOne LDA.0 OUTA 0xE9 SETD.0 AsmSelTwo LDA.0 OUTA 0xE9 BRI assembleDone assembleSelector: SETD.0 AsmSelOne LDA.0 OUTA 0xE9 BRI assembleDone assembleSelByte: SETD.0 AsmSelOne LDA.0 OUTA 0xE9 BRI assembleByte assembleSelAddress: SETD.0 AsmSelOne LDA.0 OUTA 0xE9 BRI assembleAddress assembleByte: SETD.0 TextValue INCD.0 LDA.0 OUTA 0xE9 BRI assembleDone assembleAddress: SETD.0 TextValue LDA.0 OUTA 0xE9 INCD.0 LDA.0 OUTA 0xE9 assembleDone: ; Past what was just written. The length is the shape's, out of the same table the ; disassembler reads, so the two can never disagree about how much room one takes. SETD.0 ShapeLength SETD.1 AsmShape LDB.1 assembleStep: BRB assembleStepped INCD.0 DECB BRI assembleStep assembleStepped: LDA.0 SETD.0 DumpAt CALL stepCursor RET assembleUnknown: SETD.0 NoSuchOp CALL printString CALL newLine RET assembleNeedsValue: SETD.0 NeedsValue CALL printString CALL newLine RET ; DP0 is a two byte address and A is how far to move it on. stepCursor: INCD.0 LDB.0 CCF ADD STQ.0 DECD.0 LDA.0 RSTB ADD STQ.0 RET ; The typed mnemonic into four padded characters and up to two selectors, which is the shape ; the table holds. Folded to upper case, because the assembler does not care about the case ; of a mnemonic and neither should this. ; ; A selector that was not typed is Data Pointer 0, exactly as an omitted one means in a ; source file. That is worth matching rather than demanding: half the instruction set names ; a pointer, and most code only ever uses the first. takeMnemonic: RSTA SETD.1 AsmSelOne STA.1 SETD.1 AsmSelTwo STA.1 INIA 0d4 SETD.1 AsmLeft STA.1 SETD.0 AsmLine SETD.1 AsmName takeMnemonicChar: SETD.2 AsmLeft LDA.2 BRA takeMnemonicPad ; Four is as long as a mnemonic gets. LDA.0 BRA takeMnemonicPad ; The word ended. INIB 0d46 ; A dot, so the selectors start here. XOR BRQ takeMnemonicPad ; Nothing below compares against A, so it survives all of this and is stored at the end. INIB 0d97 ; a CCF SUB BRC takeMnemonicPut ; It borrowed, so this is below 'a'. INIB 0d123 ; One past z. CCF SUB BNC takeMnemonicPut ; No borrow, so it is 'z' or later. INIB 0x20 CCF SUB MVQA ; Upper case, which is how the table holds them. takeMnemonicPut: STA.1 INCD.0 INCD.1 SETD.2 AsmLeft LDA.2 DECA STA.2 BRI takeMnemonicChar takeMnemonicPad: SETD.2 AsmLeft LDA.2 BRA takeMnemonicSelectors INIA 0x20 STA.1 INCD.1 LDA.2 DECA STA.2 BRI takeMnemonicPad takeMnemonicSelectors: RSTA STA.1 ; The four are a string now, like the ones in the table. LDA.0 INIB 0d46 XOR BNQ takeMnemonicEnd ; No dot, so both selectors stay at nought. INCD.0 LDA.0 INIB 0d48 CCF SUB MVQA SETD.1 AsmSelOne STA.1 INCD.0 LDA.0 INIB 0d46 XOR BNQ takeMnemonicEnd INCD.0 LDA.0 INIB 0d48 CCF SUB MVQA SETD.1 AsmSelTwo STA.1 takeMnemonicEnd: RET ; Looks the four characters up. AsmOpcode and AsmShape are filled in and Q is zero, or Q is ; not zero and there is no such instruction. ; ; The same table the disassembler reads, searched the other way round. That is the point of ; it being a table rather than two lists: what can be written can be read back, and what can ; be read back can be written, and neither can drift from the other. findByName: SETD.3 Instructions SETD.0 InstructionCount LDA.0 SETD.1 AsmLeft STA.1 findByNameStep: PSHD.3 POPD.0 DPUP.0 0d02 SETD.2 AsmName INIA 0d4 SETD.1 AsmCount STA.1 findByNameChar: LDA.0 LDB.2 XOR BNQ findByNameNext INCD.0 INCD.2 SETD.1 AsmCount LDA.1 DECA STA.1 BNA findByNameChar PSHD.3 POPD.0 LDA.0 SETD.1 AsmOpcode STA.1 PSHD.3 POPD.0 INCD.0 LDA.0 SETD.1 AsmShape STA.1 RSTA RSTB CCF ADD RET findByNameNext: DPUP.3 0d07 SETD.1 AsmLeft LDA.1 DECA STA.1 BNA findByNameStep RSTA INIB 0d1 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. SETD.0 BankFlags STA.0 ; Kept whole, since present is not the only thing it says. INIB 0x01 AND RET ; ---- help ---- doHelp: SETD.0 HelpText CALL printString CALL newLine SETD.0 HelpCdText CALL printString CALL newLine SETD.0 HelpScriptText CALL printString CALL newLine 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 Banner: "CosmOS" PromptText: "> " ; Nothing has run yet, so nothing has failed yet. ; Whether the line the shell is on failed. Read by the script reader and by nothing else, ; which is why it is a plain flag and not a number: a script wants to know whether to go on, ; not what went wrong, and what went wrong has already been said in words. LineFailed: 0x00 DriveWanted: 0x00 LastStatus: 0x00 OnFallback: "this is the fallback: what boot.cfg asks for did not start " NoDisk: "no filesystem on the disk" ScriptUsage: "do: give me the name of a script" ScriptNoFile: "do: cannot find it" ScriptNotOne: "do: that is not a script - it wants #! on the first line" ScriptTooDeep: "do: scripts are only four deep" DriveNoSuch: "drive: this machine has no such drive" DriveNotReadable: "drive: nothing this can read is in that drive" StartupName: "/System/Boot/startup.sh" StartupNotOne: "startup.sh is there but does not begin with #!, so it was not run" ScriptStopped: "stopped: that line did not work" Unknown: "I do not know: " Farewell: "halted" DirectoryText: "" UnfinishedText: "" BlocksText: " blocks" NotDirectory: "that is not a directory" MadeText: "made" RemovedText: "removed" MkdirWhat: "mkdir what?" RmdirWhat: "rmdir what?" MkdirNo: "cannot make that: check the path, the name, and whether it is taken" RmdirNo: "cannot remove that: it must be a directory, and empty" Separator: "/" ; Where a program is looked for when it is not where you are. One fixed place rather than a ; list somebody sets, because a list would need somewhere to live between one boot and the ; next, and there is no such place yet. ScreenFileName: "sbfs.screen" AppsPrefix: "/Apps/" SystemAppsPrefix: "0:/Apps/" IsDirectory: "that is a directory" AndText: ", " FoldersText: " directories" FolderText: " directory" FilesText: " files" FileText: " file" ; Two strings rather than one, because a string literal stops at 255 characters and each ; one carries its own zero byte, so they are printed in turn rather than joined. HelpText: "dir list what is on the disk load read a program off the disk run [words] start what was loaded, and tell it those words [words] look where you are and then in /Apps, and start that" HelpCdText: "cd [path] go to a directory, or to the root with nothing after it mkdir make a directory rmdir remove an empty one" HelpScriptText: "do run the lines in a file, which must start with #! echo [words] say them clear empty the screen" HelpMoreText: "delete take it off the disk rename call it something else monitor look at memory, change it, and jump into it help this exit stop, or leave the monitor if you are in it" ExamineUsage: "x
, or x on its own to carry on" NoSuchBank: "there is no such bank" ProgramWord: "program" DataWord: "data" ExecMagic: "SBEX" ; What every program on the disk is called, and the four characters that make one ; reachable by typing its name. SbxSuffix: ".sbx" LoadWhat: "load what?" NoSuchFile: "no such file" Deleted: "gone" Renamed: "renamed" DeleteWhat: "delete what?" RenameWhat: "rename what to what?" RenameNo: "there is no such file, or that name is taken" TooManyVectors: "that program wants more vectors than there is room for" Unreadable: "could not read it" NotProgram: "not a program" WrongVersion: "a version I do not know" LoadedText: "loaded, starting at " NothingLoaded: "nothing is loaded" Stopped: "the program was stopped" FaultOpcode: "that byte is not an instruction" FaultGuard: "a write into a fenced off part of a bank" FaultBank: "a bank that is not there, or an address past its end" FaultNoHandler: "nothing is installed at service " FaultNoDevice: "nothing is installed for the device on port " FaultAt: ", at " FaultRegisters: " A " FaultB: " B " FaultQ: " Q " FaultSystem: "that was the system itself, so there is nowhere to carry on from. Start the machine again." Finished: "finished" BreakText: "break at " ARegText: "A " BRegText: "B " QRegText: "Q " SRegText: "status " DP0Text: "DP0 " DP1Text: "DP1 " DP2Text: "DP2 " DP3Text: "DP3 " SPText: "SP " CarryText: "carry " FaultText: "fault " IntsText: "interrupts " ResumeText: "press a key " MonitorPrompt: "* " UnknownText: " ? " MonitorName: "monitor" MonitorHelp: "x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves" ExamineName: "x" DisName: "d" SetName: "s" BankName: "b" GoName: "g" BankUsage: "b " BankIs: "bank " SetUsage: "s
..." ReadOnlyText: "that bank will not be written" GoUsage: "g
" AsmName2: "a" AsmPrompt: ": " DotText: "." AsmUsage: "a
, then instructions, then a dot" NoSuchOp: "no such instruction" NeedsValue: "that one needs a value after it" DirName: "dir" DoName: "do" EchoName: "echo" ClearName: "clear" DriveName: "drive" LoadName: "load" RunName: "run" DeleteName: "delete" RenameName: "rename" CdName: "cd" MkdirName: "mkdir" RmdirName: "rmdir" HelpName: "help" ExitName: "exit" ; Where the machine is, written out for the prompt, and where in the buffer it begins. ; Built from the end backwards, so it starts somewhere in the middle. CwdText: #Reserve 0d127 CwdAt: 0x00 0x00 CwdWalk: 0x00 0x00 PathLength: 0x00 ; What is left of CwdText to write into, and whether the path ran out of it. Counted down ; rather than compared against an address, because the check is on every byte written and ; a subtraction of two pointers is a great deal more than a byte that is already going to ; be tested for zero. CwdRoom: 0x00 CwdCut: 0x00 ; What the working directory was when a program was started, so that it can be put back ; when the program stops. SavedCwd: 0x00 0x00 DirFolders: 0x00 DirTaken: 0x00 DiskReady: 0x00 LoadedOk: 0x00 ; What loadProgram found, and the words for it. See loadProgram for what the numbers mean. LoadStatus: 0x00 LoadMessage: 0x00 0x00 ; THE PATH of the file to load, which load copies out of the line as typed and a typed ; program name is built into. ; ; Sixty four rather than the twenty three a NAME needs. It held a name when a disk was ; flat and there was nothing else to hold, and leaving it that size once paths existed cut ; every path longer than twenty two characters down to twenty two - which is not a failure ; that looks like one. "/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/", ; resolved perfectly well, and reported that the program was a directory. ProgramName: #Reserve 0d64 NameOk: 0x00 NamePrefix: 0x00 NameLeft: 0x00 LoadedEntry: 0x00 0x00 LoadCount: 0x00 LoadVersion: 0x00 ; Where the first of rename's two names is, kept while the second is picked out of the ; line, since finding that needs the pointers for itself. RenameFrom: 0x00 0x00 ; What followed "run", kept for the program to ask for. 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 ; ---- Where the last file anybody asked about lives ---- ; ; osFileBlock is handed a name every time it is called, because a stateless service has no ; handle to leak and nothing left open by a program that stops in the middle. Taken at its ; word that means searching the directory once per block, so reading a four hundred block ; file walks the directory four hundred times over to be told the same thing. ; ; So the last answer is kept. A call naming the same file as the one before it skips the ; search and puts these back where sbfs keeps them. MEASURED, on the 329 block file the ; streaming test reads: 7% of the whole run saved when the file is the first entry in the ; directory, 11% when it is the sixteenth. Modest, and worth having for the shape rather ; than the size - what it really removes is a cost that grows with how full the disk is, ; on the one operation that is repeated once per block. NOTHING IS EVER TRUSTED THAT WAS NOT ; PUT HERE BY A SEARCH: this is a copy of an answer, not a second place where the truth ; about a file is written, and every path that could make it wrong calls fileForget. That ; is what makes it a speed rather than a promise - a cache that has been thrown away is ; indistinguishable from one that was never filled. ; ; The name is twenty three bytes for a name of twenty two, so that a name filling the ; field still has a zero after it and can be compared as a string. FileCacheDrive: 0x00 SearchDrive: 0x00 RunDrive: 0x00 ScreenSaved: 0x00 ScreenWasDrive: 0x00 ScreenAt: 0x00 ScreenBlockAt: 0x00 0x00 ScreenBlock: #Reserve 0d256 FileCacheValid: 0x00 FileCacheName: #Reserve 0d64 FileCacheStart: 0x00 0x00 FileCacheBlocks: 0x00 0x00 FileCacheTail: 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 ; are filled in when the program runs and read back when it exits, so what a program covers ; up comes back rather than becoming a hole. ; ; Sixteen is a limit rather than a considered number. It is far more than anything written ; so far wants, and a program asking for more is refused at load rather than having some of ; its handlers installed and the rest dropped. VectorSource: 0x00 0x00 VectorsLeft: 0x00 LoadedVectorCount: 0x00 LoadedVectors: #Reserve 0d96 ; Where the monitor is looking, so that a bare 'dump' can carry on from it. DumpBank: 0x00 DumpAt: 0x00 0x00 DumpRows: 0x00 Mode: 0x00 DumpCount: 0x00 BankWas: 0x00 BankFlags: 0x00 AsmSelOne: 0x00 AsmSelTwo: 0x00 AsmLeft: 0x00 AsmCount: 0x00 AsmOpcode: 0x00 AsmShape: 0x00 AsmName: #Reserve 0d5 AsmLine: #Reserve 0d41 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: 0d72 ; ---- 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: 0x10 0d0 "ADD " 0x11 0d0 "SUB " 0x12 0d0 "AND " 0x13 0d0 "OR " 0x14 0d0 "XOR " 0x15 0d0 "NOTA" 0x16 0d0 "NOTB" 0x17 0d0 "SHL " 0x18 0d0 "SHR " 0x60 0d1 "BRI " 0x61 0d1 "BRQ " 0x62 0d1 "BRA " 0x63 0d1 "BRB " 0x64 0d1 "BRC " 0x65 0d3 "BRD " 0x66 0d1 "BNQ " 0x67 0d1 "BNA " 0x68 0d1 "BNB " 0x69 0d1 "BNC " 0x70 0d1 "RCAL" 0x71 0d1 "CALL" 0x72 0d2 "SWI " 0x73 0d0 "RETI" 0x74 0d0 "RRET" 0x75 0d0 "RET " 0x76 0d0 "SRET" 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" 0x4E 0d3 "DPUA" 0x4F 0d3 "DPDA" 0x50 0d3 "DPUW" 0x51 0d3 "DPDW" 0xD0 0d2 "OUTQ" 0xD1 0d2 "OUTA" 0xD2 0d2 "OUTB" 0xE0 0d2 "INA " 0xE1 0d2 "INB " 0xF0 0d0 "NOP " 0xFE 0d0 "WAIT" 0xFF 0d0 "HALT" DumpBytes: #Reserve 0d16 ; Where the system's Stack was when it handed the machine to a program. Kept below the ; region a program owns, so that a program has to go looking to break it. SystemStack: 0x00 0x00 DirSeen: 0x00 DirSize: 0x00 0x00 WidthLeft: 0x00 ; ---- What a line being typed is made of ---- ; ; All of it lives here rather than being passed about, because the routine that reads a line ; is a loop over keystrokes and there are four Data Pointers, two of which it needs for the ; buffer and the walking. EditBase: 0x00 0x00 EditRoom: 0x00 EditLength: 0x00 EditAt: 0x00 EditRow: 0x00 EditColumn: 0x00 EditWidth: 0x00 EditChar: 0x00 ; Where the counting up and down the rows has got to. Separate from EditRow and EditColumn, ; which are where the LINE starts: one of the two things that reads these is working out ; that very answer, and a walk that wrote its intermediate steps into its own starting point ; would be walking away from a moving mark. EditWalkRow: 0x00 EditWalkColumn: 0x00 EditWalkBack: 0x00 ; What the control port has to be written to put the console back how this routine found it. EditWasControl: 0x00 ; ---- What the fault screen is saying ---- ; ; Whether this cause names an entry as well as itself, and which one. Only the two faults ; about a missing handler do, and for those the machine puts the number in Q. FaultNumbered: 0x00 FaultNumber: 0x00 ; Set when a program is being ended by a fault rather than by asking. handleExit reads it to ; choose its last word, and clears it. FaultStopped: 0x00 ; Whether the line being read is one to remember. The shell's are; a program's are not. EditKeepHistory: 0x00 ; How many characters of the line are on the screen, which is not always how many are in it: ; between a line getting shorter and the screen being put right, the screen still has the ; old one on it. That gap is one character wide for a Delete and a whole line wide for a ; recalled one. EditDrawn: 0x00 ; How much a redraw sent to the screen, line and rubbing out together. The start of the line ; is that far back from where the cursor ended up. EditPrinted: 0x00 ; Eight lines of a hundred and twenty eight bytes, and one more holding whatever was being ; typed when somebody pressed Up. HistoryLines: #Reserve 0d1024 HistoryTyped: #Reserve 0d128 ; Which slot holds the oldest, which is what moves when a ninth line pushes one out. HistoryStart: 0x00 ; How many of the eight hold anything. HistoryCount: 0x00 ; Which one is on the screen, counting the oldest as nought. HistoryCount means none of them ; is: what is on the screen is the line being typed. HistoryPick: 0x00 ; A hundred and twenty seven characters and the zero byte that ends them. It was sixty three ; until the shell learned to edit a line, which is when the limit started to be felt: a copy ; between two disks with a directory on each is most of the way there before anything has ; been said, and the line can now be moved about in, so a long one is worth having. CommandLine: #Reserve 0d128 #Vectors Boot boot BadOpcode faultBadOpcode GuardViolation faultGuard BankFault faultBank NoHandler faultNoHandler NoDevice faultNoDevice osPrintString handlePrintString osReadLine handleReadLine osExit handleExit osArgument handleArgument osFileRead handleFileRead osFileSave handleFileSave osFileDelete handleFileDelete osFileRename handleFileRename osFileInfo handleFileInfo osFileBlock handleFileBlock osChangeDir handleChangeDir osFileStart handleFileStart osFileWrite handleFileWrite osFileDone handleFileDone osFileFetch handleFileFetch osPrintNumber handlePrintNumber osBreak handleBreak osLastStatus handleLastStatus osTakeScreen handleTakeScreen osBootState handleBootState osBootSettle handleBootSettle Device 0x20 diskDone