; 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: ; Nobody typed this one, so there is nothing to give it. SETD.2 NoText SETD.1 ScriptArgsFrom STD.2.1 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: ; ---- Where the Stack is when nothing is happening ---- ; ; Taken once, here, and put back at the top of every turn of the loop below. SystemStack is ; not this: that is taken when a PROGRAM starts, to be given back when it stops, so it ; holds wherever the shell had got to at that moment - which is the value that needs ; correcting rather than the one to correct from. MVSD.0 SETD.1 ShellStack STD.0.1 ; ---- 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: ; ---- The Stack, back where it was when nothing was happening ---- ; ; EVERY FAILURE IN THIS SHELL ABANDONS A FRAME. commandFailed is reached with CALL and ; never returns: it marks the line and branches here, which is the idiom every command ; uses and is why a failure needs no unwinding. What it costs is the frame of that call and ; of everything between here and it - twenty bytes for a name that was never set, more from ; somewhere deeper - and nothing ever gave them back. Twenty failed lines moved the Stack ; Pointer from FFFD to FE6D, and it only ever went one way. ; ; Nothing had noticed because it takes thousands of failures to reach anything, and nobody ; types thousands of anything. A loop in a script would. ; ; So the loop starts each turn from a known place. This is the second use of MVDS in the ; system and it earns it for the same reason as the first: a Stack that is right by ; construction beats one that is right by everybody remembering. SETD.1 ShellStack LDD.0.1 MVDS.0 ; ---- What an "if" line's command made of it ---- ; ; Read here because this is where every command comes back to, and the last place a result ; is still the result of the line that produced it. It has to happen BEFORE the stop on ; failure below: a condition that fails is the ordinary half of a question, not a script ; going wrong, so the flag is cleared as it is taken. SETD.1 IfPending LDA.1 BRA promptNoIf RSTA STA.1 SETD.1 LineFailed LDA.1 BRA promptIfTaken INIA 0d1 ; It failed, so this branch is not taken and an else would be. BRI promptIfPush promptIfTaken: RSTA promptIfPush: CALL blockPush BNQ ifDeep ; A loop has to be able to come back to the line that asked, which if does not. SETD.1 LoopKind LDA.1 BRA promptIfPlain CALL blockTop BNQ promptIfPlain CALL blockKeepWhere promptIfPlain: RSTA SETD.1 LineFailed STA.1 ; And the question is answered rather than unanswered. promptNoIf: ; ---- 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 ; ---- Names filled in before anybody reads the line ---- ; ; Here rather than in the script reader, so that a typed line and a line out of a file ; behave the same and no command below has to know that variables exist. A line that names ; something nothing was ever set to does not run at all: it says so and counts as failed, ; which is what makes a mistyped name a complaint instead of an empty path. ; ; THERE IS NOTHING TO TEST AFTERWARDS, and a test was written here before that was noticed: ; commandFailed does not return. It marks the line and branches to the prompt, the way every ; command in this shell reports a failure, so the only way out of the expansion is the one ; where it worked. ; ---- Room to indent ---- ; ; Leading spaces are taken off, which the shell never allowed and never needed to: a line ; was a command and a command started at the front. Blocks change that. Nobody writes an if ; inside an if without indenting what is inside them, and a line that began with a space ; used to split into an empty first word and match nothing at all. CALL lineTrim ; ---- And a line nobody is running is not run ---- ; ; Before the names are filled in, on purpose. A branch that is not being taken must not be ; able to fail, and a name it mentions has no business existing. CALL blockSkipping BNQ blockPassOver CALL varExpand promptDispatch: 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 SetVarName CALL textSame BRQ doSetVar SETD.0 CommandLine SETD.1 IfName CALL textSame BRQ doIf SETD.0 CommandLine SETD.1 ElseName CALL textSame BRQ doElse SETD.0 CommandLine SETD.1 EndName CALL textSame BRQ doEnd SETD.0 CommandLine SETD.1 SameName CALL textSame BRQ doSame SETD.0 CommandLine SETD.1 WhileName CALL textSame BRQ doWhile SETD.0 CommandLine SETD.1 ForName CALL textSame BRQ doFor 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 INIB 0x09 XOR BRQ tabComplete ; 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: CALL editPut BNQ editKey ; No room for it. The zero byte on the end is not counted. ; At the end of the line, printing it IS the change. The insertion point is READ BACK ; rather than still being in A: it was, while this was one run of code, and a RET puts A ; back to whatever the caller had. SETD.1 EditAt LDA.1 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 ; Puts the character in EditChar into the line where the insertion point is, and moves the ; insertion point past it. DRAWS NOTHING: what that should look like depends on where in the ; line it went, and the caller is what knows. Q is zero if it went in and one if there was ; no room. ; ; A routine rather than the run of code it used to be, because Tab completing a word puts in ; several characters and every one of them is this. editPut: SETD.1 EditLength LDA.1 SETD.1 EditRoom LDB.1 CCF SUB BNC editPutFull ; 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. editPutShift: BRB editPutHere DECD.0 LDA.0 INCD.0 STA.0 DECD.0 DECB BRI editPutShift editPutHere: SETD.1 EditChar LDA.1 STA.0 SETD.1 EditLength LDA.1 INCA STA.1 SETD.1 EditAt LDA.1 INCA STA.1 RSTA RSTB CCF ADD ; Q is zero: it went in. RET editPutFull: INIA 0x01 RSTB CCF ADD ; Q is one: it did not. RET ; ---- 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 ; And quiet, before them. A program that faulted mid-note left one sounding and is no longer ; in a position to stop it. CALL soundQuiet 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 ; ---- Finishing a word somebody started ---- ; ; Tab. What it can finish depends on where in the line it is: the FIRST word is a command or ; the name of a program, and everything after it is a file. Only the first is done here. ; ; None of this could have existed a fortnight ago. The shell never saw a keystroke - the ; terminal handed it a finished line - so there was no moment at which somebody had typed ; half a word and the system knew about it. That is the same wall the history was behind. ; ; ---- What it does with what it finds ---- ; ; One match goes in with a space after it, because a word that can only be one thing is ; finished. Several are folded into their longest common prefix and that goes in, which is ; the most that can be said without guessing. If that adds nothing - because what is typed ; IS already the common prefix - the matches are listed and the line put back underneath. ; ; EVERY HELPER BELOW ANSWERS IN Q. A RET puts A, B and the first three pointers back, so a ; routine here that answered in A would be answering into a register its caller cannot see. tabComplete: ; ---- Which word, and is it the first ---- ; ; Back from the insertion point to a space or the start of the line. What lies between ; there and the cursor is what somebody has typed, and what lies before it decides whether ; this is a command being named or an argument being given. SETD.1 EditAt LDA.1 SETD.1 TabWordAt STA.1 tabBack: SETD.1 TabWordAt LDA.1 BRA tabAtLineStart ; The start of the line is the start of the word, and of the line. DECA CALL tabCharAt MVQA INIB 0x20 XOR BRQ tabFoundStart ; A space, so the word begins after it. SETD.1 TabWordAt LDA.1 DECA STA.1 BRI tabBack tabAtLineStart: INIA 0x01 SETD.1 TabFirst STA.1 BRI tabIsFirst tabFoundStart: ; There is a space behind the word, so something may be in front of that space. Only a run ; of spaces means this is still the first word - and which word it is decides what the word ; could BE: a command or a program at the front of a line, a file anywhere after it. INIA 0x01 SETD.1 TabFirst STA.1 SETD.1 TabWordAt LDA.1 SETD.1 TabLeft STA.1 tabFirstCheck: SETD.1 TabLeft LDA.1 BRA tabIsFirst DECA STA.1 CALL tabCharAt MVQA INIB 0x20 XOR BRQ tabFirstCheck RSTA SETD.1 TabFirst STA.1 ; A word came before this one, so this is an argument. tabIsFirst: ; How much of it is typed. Nothing at all is not a question anybody asked - it would offer ; every command there is, which is what help is for. SETD.1 EditAt LDA.1 SETD.1 TabWordAt LDB.1 CCF SUB BRQ tabDone MVQA SETD.1 TabWordLen STA.1 RSTA SETD.1 TabCount STA.1 SETD.1 TabBestLen STA.1 ; A path in the word says which directory to look in, and narrows what is being matched to ; the part after the last separator. CALL tabSplit ; ---- What this word could be ---- ; ; The shell's own commands, only at the front of a line and only when no directory has been ; named: nothing with a separator in it was ever going to be "dir". ; ; And on the disk: everything, wherever in the line it is. A first word used to be ; narrowed to programs, back when a program was a file ending ".sbx"; now that the shell ; runs what it reads, a first word can be any file at all. SETD.1 TabFirst LDA.1 CALL tabRunSources SETD.1 TabCount LDA.1 BRA tabDone ; Nothing matched, and saying so would be noise. ; ---- Putting it in ---- ; ; Everything of the answer past what was already typed. The insertion point is at the end ; of the word, so each character goes in where the one before it left off. SETD.1 TabWordLen LDA.1 SETD.1 TabPut STA.1 tabPutNext: SETD.1 TabPut LDA.1 SETD.1 TabBestLen LDB.1 CCF SUB ; At or past, rather than exactly equal. They cannot pass each other now - a candidate is ; only offered if it matches what was typed, so the answer is never shorter than that - but ; they DID while the answer was being taken from the wrong place, and the difference ; between the two branches was a line that ran off the end and a line that simply did not ; grow. The cheaper failure is worth a branch that costs nothing. BNC tabPutDone SETD.3 TabBest SETD.1 TabPut LDA.1 DPUA.3 LDA.3 SETD.1 EditChar STA.1 CALL editPut BNQ tabPutDone ; The line is full, so the rest of the answer will not fit. SETD.1 TabPut LDA.1 INCA STA.1 BRI tabPutNext tabPutDone: SETD.1 TabCount LDA.1 INIB 0d1 XOR BNQ tabAmbiguous ; Only one thing it can be, so it is finished and a space says so - unless it is a ; directory, which answered with a separator on the end and is somewhere to carry on typing ; rather than something to have finished. SETD.3 TabBest SETD.1 TabBestLen LDA.1 DECA DPUA.3 LDA.3 INIB 0x2F XOR BRQ tabSaidDirectory INIA 0x20 SETD.1 EditChar STA.1 CALL editPut tabSaidDirectory: CALL editRedraw BRI tabDone tabAmbiguous: ; Several. The common prefix has gone in; if it added nothing then what was typed is ; already as far as they all agree, and the only useful thing left is to show them. SETD.1 TabBestLen LDA.1 SETD.1 TabWordLen LDB.1 CCF SUB BRQ tabList CALL editRedraw tabDone: BRI editKey ; Everywhere a candidate can come from, in the order they are tried. Run once to find the ; answer and again to show the matches, which is why it is a routine and not a run of code. tabRunSources: SETD.1 TabFirst LDA.1 BRA tabNoNames SETD.1 TabDirLen LDA.1 BNA tabNoNames CALL tabTryNames tabNoNames: CALL tabTryFiles RET ; A holds an offset into the line. Q is the character there. tabCharAt: SETD.1 EditBase LDD.0.1 DPUA.0 LDA.0 RSTB CCF ADD RET ; ---- Offering a candidate ---- ; ; DP3 names a string ending in a zero. If it begins with what has been typed it is folded ; into the answer, and DP3 is left just past that zero either way - which is what makes a run ; of packed strings walkable by calling this over and over. tabOffer: ; Where this candidate begins, before the comparison below walks DP3 through it. What is ; taken as the answer is the WHOLE name and not the part after the bit that matched. SETD.1 TabStart STD.3.1 SETD.1 TabWordLen LDA.1 SETD.1 TabSeen STA.1 RSTA SETD.1 TabAt STA.1 tabOfferMatch: SETD.1 TabSeen LDA.1 BRA tabOfferTake ; Every typed character agreed. DECA STA.1 LDA.3 BRA tabOfferSkip ; The candidate ended before what was typed did. SETD.1 TabHold STA.1 ; Kept, while the line's character is fetched. SETD.1 TabWordAt LDA.1 SETD.1 TabAt LDB.1 CCF ADD MVQA CALL tabCharAt MVQA SETD.1 TabHold LDB.1 XOR BNQ tabOfferSkip INCD.3 SETD.1 TabAt LDA.1 INCA STA.1 BRI tabOfferMatch tabOfferTake: ; ---- Folded in, or shown ---- ; ; The same walk answers both questions, because they are the same question asked twice: ; what matches. Listing used to be a second copy of the walk that knew only about the ; shell's own words, so the files it should have been showing were never there. SETD.1 TabShowing LDA.1 BNA tabOfferPrint ; The first match is the answer. Every one after it cuts the answer back to where the two ; stop agreeing, which is all that can be said without guessing which was meant. SETD.1 TabCount LDA.1 INCA STA.1 INIB 0d1 XOR BRQ tabOfferFirst CALL tabNarrow BRI tabOfferSkip tabOfferFirst: CALL tabTake BRI tabOfferSkip tabOfferPrint: SETD.1 TabStart LDD.0.1 CALL printString INIA 0x20 OUTA 0x00 OUTA 0x00 ; Two spaces between them, and A already holds one. tabOfferSkip: ; Past the end of this candidate, from wherever the comparison stopped. LDA.3 BRA tabOfferPast INCD.3 BRI tabOfferSkip tabOfferPast: INCD.3 RET ; The candidate DP3 names becomes the answer. DP3 is put somewhere and read back rather than ; walked, because it is the only pointer that survives a call and the caller still needs it. tabTake: SETD.1 TabStart LDD.0.1 SETD.2 TabBest RSTA SETD.1 TabBestLen STA.1 tabTakeNext: SETD.1 TabBestLen LDA.1 INIB 0d63 CCF SUB BNC tabTakeDone ; As much of a name as there is room for. LDA.0 BRA tabTakeDone STA.2 INCD.0 INCD.2 LDA.1 INCA STA.1 BRI tabTakeNext tabTakeDone: RET ; The answer is cut back to where it and the candidate DP3 names stop agreeing. tabNarrow: SETD.1 TabStart LDD.0.1 SETD.2 TabBest RSTA SETD.1 TabNarrowAt STA.1 tabNarrowNext: SETD.1 TabNarrowAt LDA.1 SETD.1 TabBestLen LDB.1 CCF SUB BNC tabNarrowDone ; The answer ran out first, so all of it still agrees. LDA.0 BRA tabNarrowCut ; The candidate ran out, so the answer stops here. LDB.2 XOR BNQ tabNarrowCut INCD.0 INCD.2 SETD.1 TabNarrowAt LDA.1 INCA STA.1 BRI tabNarrowNext tabNarrowCut: SETD.1 TabNarrowAt LDA.1 SETD.1 TabBestLen STA.1 tabNarrowDone: RET ; ---- Splitting the word at its last separator ---- ; ; "Apps/Sn" is a directory to look in and a name to match, and everything downstream depends ; on getting that split right. What comes back is TabDir holding the directory part - empty ; when there is no separator, meaning where the machine already is - and TabWordAt and ; TabWordLen narrowed to the name part, so that matching and inserting go on working on the ; part of the word they were always about. tabSplit: RSTA SETD.1 TabDirLen STA.1 ; Backwards, so the LAST separator is the one found. SETD.1 TabWordLen LDA.1 SETD.1 TabAt STA.1 tabSplitBack: SETD.1 TabAt LDA.1 BRA tabSplitNone ; No separator anywhere in it. DECA STA.1 SETD.1 TabWordAt LDB.1 CCF ADD MVQA CALL tabCharAt MVQA INIB 0x2F XOR BNQ tabSplitBack ; Found one. The directory part is everything up to and including it, which is what makes ; "/Source/" and "Source/" both say what they mean. SETD.1 TabAt LDA.1 INCA SETD.1 TabDirLen STA.1 SETD.2 TabDir RSTA SETD.1 TabCopyAt STA.1 tabSplitCopy: SETD.1 TabCopyAt LDA.1 SETD.1 TabDirLen LDB.1 CCF SUB BNC tabSplitCopied SETD.1 TabWordAt LDA.1 SETD.1 TabCopyAt LDB.1 CCF ADD MVQA CALL tabCharAt MVQA STA.2 INCD.2 SETD.1 TabCopyAt LDA.1 INCA STA.1 BRI tabSplitCopy tabSplitCopied: RSTA STA.2 ; The zero that ends it. ; And the word being matched is what comes after the separator. SETD.1 TabWordAt LDA.1 SETD.1 TabDirLen LDB.1 CCF ADD MVQA SETD.1 TabWordAt STA.1 SETD.1 TabWordLen LDA.1 SETD.1 TabDirLen LDB.1 CCF SUB MVQA SETD.1 TabWordLen STA.1 RET tabSplitNone: SETD.0 TabDir RSTA STA.0 RET ; ---- What is on the disk ---- ; ; Every entry of the directory the word names that begins with the rest of the word. A ; directory is offered with a separator after it, which says what it is and lets the next ; part be typed straight away - and, because the answer then ends in one, is also what stops ; a space being put after it. ; ; WHERE THE MACHINE IS COMES BACK. This walks somebody else's directory by standing in it, ; which is the only way to walk one here, so where they were and which drive they were on are ; put down first and restored whatever happens. tabTryFiles: SETD.0 DiskReady LDA.0 BRA tabFilesNone INA 0x24 SETD.1 TabWasDrive STA.1 SETD.0 SbfsCwd SETD.1 TabWasCwd CALL sbfsCopyWord CALL tabWalkNamed ; ---- And the places a program is looked for ---- ; ; The first word of a line is a command or a program, and the shell looks for a program in ; three places: where you are, the system's own place for them on the disk you are on, and ; that same place on drive 0. Tab offers what the shell would find, or it would finish a ; word into something that then would not run. ; ; ONLY WHEN NO DIRECTORY WAS NAMED. "Apps/Sn" means that directory and nowhere else. SETD.1 TabFirst LDA.1 BRA tabFilesBack SETD.1 TabDirLen LDA.1 BNA tabFilesBack CALL tabWalkApps ; And drive 0's, unless that is the drive we are already on - in which case it is the same ; directory, and offering everything in it twice would mean nothing was ever the only ; match. SETD.1 TabWasDrive LDA.1 BRA tabFilesBack RSTA CALL sbfsUse CALL tabWalkApps BRI tabFilesBack ; ---- Everything in a directory, offered ---- ; ; Two ways in, because there are two kinds of place to look: the one the word named, and the ; system's own place for programs. THE FIRST MUST NOT BE OVERWRITTEN BY THE SECOND - an ; earlier version wrote "/Apps" into TabDir, and since the whole walk runs a second time to ; list the matches, that second run looked in /Apps for a word which had named nowhere, and ; showed nothing at all. ; ; Where the machine stands is moved and put back by the caller, which is what makes calling ; either of these more than once safe. tabWalkApps: SETD.0 TabWasCwd SETD.1 SbfsCwd CALL sbfsCopyWord SETD.0 AppsPath BRI tabWalkTo tabWalkNamed: SETD.0 TabWasCwd SETD.1 SbfsCwd CALL sbfsCopyWord SETD.1 TabDirLen LDA.1 BRA tabFilesHere ; No directory named, so it is this one. SETD.0 TabDir tabWalkTo: CALL sbfsWalk BNQ tabWalkNothing ; No such path, and nothing to offer out of it. ; The root is not an entry, so it has no flags to ask - and it is the one place that can be ; walked to without there being anything to have asked. SETD.0 SbfsAt LDA.0 INCD.0 LDB.0 OR BRQ tabFilesGo SETD.0 SbfsFoundFlags LDA.0 INIB 0x02 AND BRQ tabWalkNothing ; Not a directory, so there is nothing in it. tabFilesGo: SETD.0 SbfsAt SETD.1 SbfsCwd CALL sbfsCopyWord tabFilesHere: CALL sbfsFirst BRI tabFilesCheck tabFilesStep: CALL sbfsNext tabFilesCheck: BNQ tabWalkNothing CALL tabEntry BRI tabFilesStep tabWalkNothing: RET tabFilesBack: SETD.0 TabWasCwd SETD.1 SbfsCwd CALL sbfsCopyWord SETD.1 TabWasDrive LDA.1 CALL sbfsUse tabFilesNone: RET ; The entry the walk is standing on, offered under the name somebody would type for it. tabEntry: SETD.0 SbfsName SETD.1 TabCandidate CALL tabCopyName SETD.0 SbfsFoundFlags LDA.0 INIB 0x02 AND BRQ tabEntryFile ; The bit SET is what says directory - see dir, which asks the ; same question the same way round. ; A directory, and the separator goes on the end of it. SETD.3 TabCandidate CALL tabEnd INIA 0x2F STA.3 INCD.3 RSTA STA.3 tabEntryFile: ; A file, offered under the name it actually has - so nothing happens to it here at all. ; A first word used to be offered with ".sbx" taken off, and anything without that ending ; not offered, which was right while the extension was what made a file reachable by name. ; THE SHELL NOW RUNS WHAT IT READS, so a script and a launcher start by name too, and ; hiding them would hide exactly the things a first word is most likely to be. SETD.3 TabCandidate CALL tabOffer RET ; DP0 names a string and DP1 where to put it. Copies up to what a name can be. tabCopyName: RSTB tabCopyNext: LDA.0 BRA tabCopyDone STA.1 INCD.0 INCD.1 INCB INIA 0d40 CCF SUB BRQ tabCopyDone BRI tabCopyNext tabCopyDone: RSTA STA.1 RET ; DP3 names a string. Leaves DP3 on its zero. tabEnd: LDA.3 BRA tabEndDone INCD.3 BRI tabEnd tabEndDone: RET ; ---- The shell's own words ---- ; ; Fifteen strings packed end to end, each ending in the zero that says where the next one ; begins. That is why they are laid out that way: a chain of comparisons can be executed and ; cannot be walked. tabTryNames: SETD.1 ShellNameCount LDA.1 SETD.1 TabLeft STA.1 SETD.3 ShellNames tabNameNext: SETD.1 TabLeft LDA.1 BRA tabNamesDone DECA STA.1 CALL tabOffer BRI tabNameNext tabNamesDone: RET ; ---- Showing them when they cannot be narrowed further ---- ; ; The line is reprinted underneath afterwards, and where it now BEGINS is asked rather than ; remembered: the prompt has just been printed somewhere else entirely, and the whole of the ; editor's idea of where things are hangs off those two registers. tabList: CALL newLine INIA 0x01 SETD.1 TabShowing STA.1 CALL tabRunSources RSTA SETD.1 TabShowing STA.1 CALL newLine CALL sayPrompt INA 0x03 SETD.1 EditRow STA.1 INA 0x04 SETD.1 EditColumn STA.1 RSTA SETD.1 EditDrawn STA.1 ; Nothing of the line is on the screen at its new place. CALL editRedraw BRI editKey ; ---- Things a line can be given a name for ---- ; ; Eight of them, so a script can hold a path or a half written command and compose the rest ; around it later. Each is one record of sixty four bytes: sixteen of name, then forty eight ; of value. SIXTY FOUR RATHER THAN EIGHTY, because A and B are a sixteen bit shift register ; and two rotations right turn a slot number into the offset of its slot - the same trick the ; history uses, and the reason neither of them needs a multiply this machine has not got. ; ; A slot whose name begins with a zero is free. Nothing counts them and nothing has to be ; kept in step. ; ; ---- A name that was never set is an error, and says so ---- ; ; Expanding it to nothing is what other shells do and it is the wrong answer here: a mistyped ; name would quietly become an empty path, and everything else in this system spends its ; effort on saying what went wrong. Somebody who genuinely wants an empty value writes ; "set name" with nothing after it and gets one - so the escape hatch exists, and has to be ; asked for rather than arrived at by accident. ; DP3 to variable slot A, which is 0 to 7. varSlotAt: SETD.3 VarSlots RSTB SHR SHR ; A and B together are the slot number times sixty four. DPUW.3 RET ; DP0 names a name. Q is zero if it is there, and DP3 is left on its slot. varFind: SETD.1 VarWanted STD.0.1 RSTA SETD.1 VarSlot STA.1 varFindNext: SETD.1 VarSlot LDA.1 INIB 0d8 CCF SUB BNC varFindNo ; Past the last slot. LDA.1 CALL varSlotAt SETD.1 VarWanted LDD.0.1 LDA.3 BRA varFindStep ; A free slot has no name to be the one wanted. varFindMatch: LDA.0 BRA varFindEnded LDB.3 XOR BNQ varFindStep INCD.0 INCD.3 BRI varFindMatch varFindEnded: ; The same only if the slot's name ends here too. "path" and "pathname" are two names, and ; comparing until the shorter runs out would call them one. LDA.3 BNA varFindStep SETD.1 VarSlot LDA.1 CALL varSlotAt RSTA RSTB CCF ADD ; Q is zero: found. RET varFindStep: SETD.1 VarSlot LDA.1 INCA STA.1 BRI varFindNext varFindNo: INIA 0x01 RSTB CCF ADD ; Q is one: no such name. RET ; DP3 names a slot. Moves it on to that slot's value. varValueOf: INIA 0d16 DPUA.3 RET ; DP0 names a name and DP1 its value. Q is zero if it was written down. varSet: SETD.2 VarKeepValue STD.1.2 ; The value, while a slot is looked for. CALL varFind BRQ varSetInto ; Already there, so it is written over. ; A free one, which is any whose name begins with a zero. RSTA SETD.1 VarSlot STA.1 varSetFree: SETD.1 VarSlot LDA.1 INIB 0d8 CCF SUB BNC varSetFull LDA.1 CALL varSlotAt LDA.3 BRA varSetInto SETD.1 VarSlot LDA.1 INCA STA.1 BRI varSetFree varSetInto: ; Which slot it is, is in VarSlot either way: varFind leaves the one it found there, and ; the search above leaves the free one it stopped on. So it is worked out again rather than ; carried in a pointer through two calls that would each put it back. SETD.1 VarSlot LDA.1 CALL varSlotAt PSHD.3 POPD.1 INIA 0d15 CALL varCopy ; DP0 is still the name. SETD.1 VarSlot LDA.1 CALL varSlotAt CALL varValueOf PSHD.3 POPD.1 SETD.2 VarKeepValue LDD.0.2 INIA 0d47 CALL varCopy RSTA RSTB CCF ADD ; Q is zero: it is written down. RET varSetFull: INIA 0x01 RSTB CCF ADD ; Q is one: there is no room for another. RET ; DP0 to DP1, at most A characters and then the zero that ends them. varCopy: SETD.2 VarRoom STA.2 varCopyNext: SETD.2 VarRoom LDA.2 BRA varCopyDone DECA STA.2 LDA.0 BRA varCopyDone STA.1 INCD.0 INCD.1 BRI varCopyNext varCopyDone: RSTA STA.1 RET ; A holds a character. Q is zero if it is one a name may be made of, which is what decides ; where a name written in the middle of a path stops: "$where/file" is a name and a path. varNameChar: INIB 0x30 CCF SUB BRC varNameCharNo ; Below a digit. INIB 0x3A CCF SUB BRC varNameCharYes ; A digit. INIB 0x41 CCF SUB BRC varNameCharNo INIB 0x5B CCF SUB BRC varNameCharYes ; A capital. INIB 0x61 CCF SUB BRC varNameCharNo INIB 0x7B CCF SUB BRC varNameCharYes ; A small letter. varNameCharNo: INIA 0x01 RSTB CCF ADD RET varNameCharYes: RSTA RSTB CCF ADD RET ; ---- The line, with every name in it replaced by what it stands for ---- ; ; Done here, on its way from wherever it was read to whoever runs it, so that a script line ; and a typed line behave the same and no command has to know that variables exist at all. ; The same shape as the line editing: one place the whole system already flows through. ; ; WHERE THE OUTPUT HAS GOT TO IS KEPT IN MEMORY rather than in a pointer. There are four ; pointers and this needs the line being read, the name being gathered, the value being ; copied and somewhere to put each character - and the moment the output position lived in ; one of them, gathering a name reset it to the start of the line. ; ; Q is zero if the line is ready to run. varExpand: SETD.0 VarLine SETD.1 VarPut STD.0.1 RSTA SETD.1 VarUsed STA.1 SETD.0 CommandLine varExpandNext: LDA.0 BRA varExpandDone INIB 0x24 XOR BRQ varExpandName CALL varExpandPut BNQ varExpandLong INCD.0 BRI varExpandNext varExpandName: ; Past the marker, and then as much of a name as follows it. INCD.0 SETD.2 VarName RSTA SETD.1 VarNameLen STA.1 varExpandNameChar: LDA.0 CALL varNameChar BNQ varExpandNamed SETD.1 VarNameLen LDA.1 INIB 0d15 CCF SUB BNC varExpandNameLong ; One more and there would be nowhere to put it. LDA.0 STA.2 INCD.2 INCD.0 LDA.1 INCA STA.1 BRI varExpandNameChar varExpandNamed: RSTA STA.2 ; The zero that ends the gathered name. SETD.1 VarNameLen LDA.1 BNA varExpandLookUp ; A marker with nothing after it is just a marker, and goes through as one rather than ; being an error about a variable nobody mentioned. INIA 0x24 CALL varExpandPut BNQ varExpandLong BRI varExpandNext varExpandLookUp: SETD.1 VarKeepLine STD.0.1 ; Where the line has got to, across the search. ; A script's own names are asked about first, so that a parameter cannot be shadowed by ; something set at the prompt. Only inside a script: at the prompt "$1" is a name nobody ; set, and gets told so like any other. CALL varScriptName BRQ varExpandFound SETD.0 VarName CALL varFind BNQ varExpandNoSuch CALL varValueOf varExpandFound: SETD.1 VarKeepLine LDD.0.1 varExpandValue: LDA.3 BRA varExpandNext CALL varExpandPut BNQ varExpandLong INCD.3 BRI varExpandValue varExpandDone: RSTA CALL varExpandPut ; The zero that ends the built line. BNQ varExpandLong ; And back where everything downstream looks for it. SETD.0 VarLine SETD.1 CommandLine INIA 0d127 CALL varCopy RSTA RSTB CCF ADD RET ; A holds a character. Puts it where the built line has got to. Q is one if there is no room, ; which is a line that grew past what the shell can hold once its names were filled in. varExpandPut: SETD.2 VarHold STA.2 SETD.2 VarUsed LDA.2 INIB 0d127 CCF SUB BNC varExpandNoRoom SETD.2 VarPut LDD.1.2 SETD.2 VarHold LDA.2 STA.1 INCD.1 SETD.2 VarPut STD.1.2 SETD.2 VarUsed LDA.2 INCA STA.2 RSTA RSTB CCF ADD RET varExpandNoRoom: INIA 0x01 RSTB CCF ADD RET ; ---- A name too long to be one is not a shorter name ---- ; ; Cutting it off at fifteen characters is what this did, and it is the quiet wrong answer ; this whole feature was built to avoid: two names that differ only after the fifteenth ; character would be ONE variable, and the message about a missing one would name something ; the person never typed. varExpandNameLong: SETD.0 VarNameLong CALL printString CALL newLine BRI commandFailed varExpandLong: SETD.0 VarTooLong CALL printString CALL newLine BRI commandFailed ; ---- The names a script gets for nothing ---- ; ; "$args" is everything it was given, and "$1" to "$9" are the words of that, counted from ; one. Nothing is stored per parameter: the line it was given is kept whole and the word ; wanted is walked out of it, so there is no limit on how many a script may be handed and ; nothing to keep in step. ; ; A NAME ONE OF THESE MISSES IS AN ERROR, like every other name this shell does not know. ; "$2" in a script given one word says so and stops it, rather than quietly becoming nothing ; and letting a command run with an argument missing - which is the rule set.asm's own comment ; argues for at length, applied to the names a script did not have to set. ; ; Q is zero if VarName is one of them, and DP3 is left on the value. DP3 because a RET puts ; the others back, which is the same reason varValueOf answers there. varScriptName: SETD.1 ScriptDepth LDA.1 BRA varScriptNo ; Not in a script, so these names mean nothing here. SETD.0 VarName SETD.1 ArgsWord CALL textSame BRQ varScriptAll ; One character, and that character a digit from one to nine. Testing the length first is ; what keeps "1st" from being read as "1". SETD.0 VarName INCD.0 LDA.0 BNA varScriptNo SETD.0 VarName LDA.0 INIB 0x31 CCF SUB BRC varScriptNo ; Below '1'. There is no word nought. LDA.0 INIB 0x3A CCF SUB BNC varScriptNo ; Above '9'. LDA.0 INIB 0x30 CCF SUB MVQA ; Which word is wanted, one to nine. CALL varScriptWord RET varScriptAll: SETD.3 ScriptArgs RSTA RSTB CCF ADD ; Q is zero: it is one of them. RET varScriptNo: INIA 0x01 RSTB CCF ADD RET ; A is which word is wanted. Copies it out into VarWord and leaves DP3 on it, because the ; words are separated by spaces rather than ended by zeroes and everything downstream of here ; walks a string until it ends. ; ; Q is one if the script was not given that many, which reads as a name that is not set. varScriptWord: SETD.1 VarWordWanted STA.1 SETD.0 ScriptArgs varScriptSkip: LDA.0 BRA varScriptShort ; The end, and the word wanted is past it. INIB 0x20 CCF SUB BNQ varScriptWordAt ; Not a space, so a word begins here. INCD.0 BRI varScriptSkip varScriptWordAt: SETD.1 VarWordWanted LDA.1 DECA STA.1 BRA varScriptTake ; Counted down to nought, so this is the one. varScriptPast: LDA.0 BRA varScriptShort INIB 0x20 CCF SUB BRQ varScriptSkip ; The end of this word, so the next one is looked for. INCD.0 BRI varScriptPast varScriptTake: SETD.1 VarWord varScriptTakeChar: LDA.0 BRA varScriptTaken INIB 0x20 CCF SUB BRQ varScriptTaken LDA.0 STA.1 INCD.0 INCD.1 BRI varScriptTakeChar varScriptTaken: RSTA STA.1 ; The zero that makes it a string. SETD.3 VarWord RSTB CCF ADD ; Q is zero, A having been reset above. RET varScriptShort: INIA 0x01 RSTB CCF ADD RET varExpandNoSuch: SETD.0 VarNoSuch CALL printString SETD.0 VarName CALL printString CALL newLine BRI commandFailed ; ---- Lines that are only run sometimes ---- ; ; "if" takes a COMMAND and runs it, and what follows is run only if that command worked. That ; is the Bourne shell's answer and it is the reason "test" exists there: one rule in if, and ; comparing two things is just another command that can fail. Here that command is "same", ; and the shell already had the other half - LineFailed, which every command sets and which ; stop-on-failure was built on. ; ; A block is a record of sixteen bytes, eight of them, and sixteen because A and B are a ; shift register: four rotations turn a block number into its offset. The history and the ; variables are addressed the same way and for the same reason. ; ; +0 state: 0 running, 1 not but an else would, 2 not and an else would not ; +1 kind: 0 if, 1 while, 2 for ; +2 how many words a for has used ; +4 where the line that opened it was, as a script position - see blockKeepWhere ; ; The two kinds of not-running are what make nesting work without looking down the stack: an ; "if" met while something above it is being skipped pushes a 2, so the top of the stack ; always says everything, and a line runs when the stack is empty or its top is nought. ; DP3 to block A. blockAt: SETD.3 BlockStack RSTB SHR SHR SHR SHR DPUW.3 RET ; DP3 on the block on top. Q is one if there is not one. blockTop: SETD.1 BlockDepth LDA.1 BRA blockNone DECA CALL blockAt RSTA RSTB CCF ADD RET blockNone: INIA 0x01 RSTB CCF ADD RET blockSkipping: CALL blockTop BNQ blockRunning ; Nothing open, so nothing is being skipped. LDA.3 BRA blockRunning INIA 0x01 RSTB CCF ADD ; Q is one: skipping. RET blockRunning: RSTA RSTB CCF ADD ; Q is zero: running. RET ; A holds the state to push and LoopKind what kind of block it is. Q is one if there is no ; room for another. blockPush: SETD.1 BlockHold STA.1 SETD.1 BlockDepth LDA.1 INIB 0d8 CCF SUB BNC blockFull LDA.1 CALL blockAt SETD.1 BlockHold LDA.1 STA.3 INCD.3 SETD.1 LoopKind LDA.1 STA.3 INCD.3 RSTA STA.3 ; No words used yet, whatever kind it is. SETD.1 BlockDepth LDA.1 INCA STA.1 RSTA RSTB CCF ADD RET blockFull: INIA 0x01 RSTB CCF ADD RET blockPop: SETD.1 BlockDepth LDA.1 DECA STA.1 RET ; ---- Going back to the line that opened a block ---- ; ; A loop is a block that, when it ends, puts the reader back where it started. The position ; was kept before the line was read - see ScriptLineIndex in script.asm, because by the time ; a line has been read the reader is past it and a line is not a fixed size to subtract. The ; block it names is read again on the way back, which is what makes the pointer into it mean ; what it meant. Exactly what nesting one script inside another already does, for a different ; reason. ; ; DP3 names the block. blockKeepWhere: SETD.0 ScriptLineIndex PSHD.3 POPD.1 DPUP.1 0d4 CALL sbfsCopyWord SETD.0 ScriptLineAt PSHD.3 POPD.1 DPUP.1 0d6 CALL sbfsCopyWord SETD.0 ScriptLineBlocks PSHD.3 POPD.1 DPUP.1 0d8 CALL sbfsCopyWord RET blockGoWhere: PSHD.3 POPD.0 DPUP.0 0d4 SETD.1 ScriptIndex CALL sbfsCopyWord PSHD.3 POPD.0 DPUP.0 0d6 SETD.1 ScriptAt CALL sbfsCopyWord PSHD.3 POPD.0 DPUP.0 0d8 SETD.1 ScriptBlocks CALL sbfsCopyWord CALL scriptReread RET ; ---- if ---- ; ; The rest of the line is a command, so the line becomes that command and is dispatched ; again, with a note saying that what it makes of it decides a block rather than being ; somebody's answer. The note is read at the top of the loop, which is where every command ; comes back to and the one place that sees a result before the next line disturbs it. doIf: RSTA SETD.1 LoopKind STA.1 doIfKind: CALL blockSkipping BNQ ifSkipped SETD.1 TextRest LDD.0.1 LDA.0 BRA ifWhat ; Nothing to decide by. ; The rest of the line, moved to the front of it. Forwards, and the source is ahead of the ; destination, so one walk does it without anything being overwritten before it is read. SETD.1 CommandLine ifShift: LDA.0 STA.1 BRA ifShifted INCD.0 INCD.1 BRI ifShift ifShifted: INIA 0x01 SETD.1 IfPending STA.1 BRI promptDispatch ifSkipped: ; Inside something that is not being taken. The condition is not run and not even looked ; at - a line that is not being taken must not be able to fail, or a name it mentions ; would have to exist. INIA 0d2 CALL blockPush BNQ ifDeep BRI prompt ifWhat: SETD.0 IfUsage BRI blockComplain ifDeep: SETD.0 IfTooDeep BRI blockComplain ; ---- while ---- ; ; The same shape as if, and then one thing more: the block remembers where the while line ; was, and end goes back to it - so the condition is ASKED AGAIN rather than remembered. doWhile: SETD.1 ScriptDepth LDA.1 BRA loopTyped ; There is nothing to go back to at a prompt. INIA 0d1 SETD.1 LoopKind STA.1 BRI doIfKind ; Neither loop means anything typed at a prompt: there is no line to go back to. Said once, ; because both of them arrive here and a message naming the wrong one is worse than none. loopTyped: SETD.0 LoopTyped BRI blockComplain ; ---- else ---- doElse: CALL blockTop BNQ elseLonely LDA.3 BRA elseTaken ; This branch ran, so the other one does not. INIB 0d1 XOR BNQ elseStays ; A two stays a two: nothing here is being taken. RSTA STA.3 BRI prompt elseTaken: INIA 0d2 STA.3 elseStays: BRI prompt elseLonely: SETD.0 ElseLonely BRI blockComplain ; ---- end ---- ; ; What it does depends on what it closes. An if is simply taken away. A loop that was running ; goes back to the line that opened it - a while so that its condition is asked again, and a ; for so that its next word is taken - and a loop that was not running has finished, so it ; goes away like anything else. doEnd: CALL blockTop BNQ endLonely LDA.3 BNA endAway ; Not running, so whatever it was, it is over. INCD.3 LDA.3 BRA endAway ; An if that ran needs nothing doing. INIB 0d2 XOR BRQ endForAgain ; A while: taken away, and the reader put back on its line. The line pushes a fresh block ; when it asks its question again. CALL blockTop CALL blockGoWhere CALL blockPop BRI prompt endForAgain: ; A for: the block stays, because what it has used is in it, and the line it opened with ; reads itself again and counts off one more word. INIA 0x01 SETD.1 LoopResume STA.1 CALL blockTop CALL blockGoWhere BRI prompt endAway: CALL blockPop BRI prompt endLonely: SETD.0 EndLonely BRI blockComplain ; ---- for ---- ; ; "for x in a b c" runs what follows once for each word, with x set to it in turn. THE LINE IS ; READ AGAIN ON EVERY TURN and the words counted off from the front, so the only thing a block ; has to remember is how many have been used - one byte, rather than a copy of the list in ; every block. doFor: SETD.1 ScriptDepth LDA.1 BRA loopTyped SETD.1 LoopResume LDA.1 BNA forAgain CALL blockSkipping BNQ ifSkipped ; A new one. It is pushed running, and set to not-running below if there are no words. INIA 0d2 SETD.1 LoopKind STA.1 RSTA CALL blockPush BNQ ifDeep CALL blockTop BNQ forLonely CALL blockKeepWhere BRI forWord forAgain: ; Coming round again: the block that is open already says how many words have gone. RSTA SETD.1 LoopResume STA.1 CALL blockTop BNQ forLonely INIA 0d2 DPUA.3 LDA.3 INCA STA.3 forWord: CALL blockTop BNQ forLonely INIA 0d2 DPUA.3 LDA.3 SETD.1 ForUsed STA.1 SETD.1 TextRest LDD.0.1 SETD.1 ForVarAt STD.0.1 CALL textSplit ; The word "in" between the name and the words. Split off first and then compared, because ; textSame asks whether two whole strings are the same and "in red green blue" is not "in". SETD.1 TextRest LDD.0.1 LDA.0 BRA forWhat CALL textSplit SETD.1 InName CALL textSame BNQ forWhat forSkipWords: SETD.1 ForUsed LDA.1 BRA forTakeWord DECA STA.1 SETD.1 TextRest LDD.0.1 LDA.0 BRA forNoMore CALL textSplit BRI forSkipWords forTakeWord: SETD.1 TextRest LDD.0.1 LDA.0 BRA forNoMore SETD.1 ForWordAt STD.0.1 CALL textSplit SETD.0 ForVarAt LDD.0.0 SETD.1 ForWordAt LDD.1.1 CALL varSet BNQ forFull CALL blockTop BNQ forLonely RSTA STA.3 ; Running, and the body follows. BRI prompt forNoMore: ; The words have run out, so the body is passed over and the end takes the block away. CALL blockTop BNQ forLonely INIA 0d1 STA.3 BRI prompt forWhat: SETD.0 ForUsage BRI blockComplain forFull: SETD.0 SetVarNoRoom BRI blockComplain forLonely: SETD.0 EndLonely BRI blockComplain ; ---- same ---- ; ; Two words, and it fails when they differ. A command rather than a form of if, so that if ; has one rule and anything that can fail can be asked about. doSame: SETD.1 TextRest LDD.0.1 SETD.1 SameFirst STD.0.1 CALL textSplit SETD.0 SameFirst LDD.0.0 SETD.1 TextRest LDD.1.1 CALL textSame BNQ sameNot BRI prompt sameNot: BRI commandFailed ; DP0 names what went wrong. Says it and marks the line, which is what every one of these ; does and is worth writing once. blockComplain: CALL printString CALL newLine BRI commandFailed ; Takes the spaces off the front of the line, by moving what is after them to the front. In ; place and forwards, with the source ahead of the destination, so one walk does it. lineTrim: SETD.0 CommandLine LDA.0 INIB 0x20 XOR BNQ lineTrimDone ; It does not start with one, which is almost every line. lineTrimSkip: INCD.0 LDA.0 BRA lineTrimEmpty INIB 0x20 XOR BRQ lineTrimSkip SETD.1 CommandLine lineTrimMove: LDA.0 STA.1 BRA lineTrimDone INCD.0 INCD.1 BRI lineTrimMove lineTrimEmpty: ; Nothing but spaces. It becomes an empty line, which the shell already knows to do ; nothing about. SETD.0 CommandLine RSTA STA.0 ; ---- And the space at the other end, which finishing a word leaves behind ---- ; ; Tab completion puts a space after the word it finished, because that is what you want when ; another word is coming. When nothing else is coming, that space stayed on the end of the ; line, and a command that takes a file name looked for one whose name ended in a space: ; ; load greet.sbx loaded, starting at 5000 ; load greet.sbx no such file (the same line, completed with Tab) ; ; Which took most of the good out of completion, since finishing a name and pressing Return ; is the whole of what it is for. Taken off the WHOLE LINE rather than at each command that ; takes a name, for the same reason the leading spaces are: there are a dozen of those and ; they should not each have to know. ; ; SAFE TO WALK BACKWARDS WITHOUT A GUARD, and that is worth saying because it looks like it ; is not. Everything above has already run, so the first character is not a space - either ; the line never began with one, or the move loop took them off, or the line was nothing but ; spaces and the empty test below sends it home. So the walk back always meets a character ; that stops it before it reaches the front. lineTrimDone: SETD.0 CommandLine LDA.0 BRA lineTrimEnded ; An empty line has no end to tidy. lineTrimSeek: INCD.0 LDA.0 BNA lineTrimSeek ; On the zero that ends the line. lineTrimBack: DECD.0 LDA.0 INIB 0x20 XOR BNQ lineTrimEnded ; Not a space, so this is where the line really ends. RSTA STA.0 ; The space becomes the end of it, and the one before may go too. BRI lineTrimBack lineTrimEnded: RET ; DP0 names a line and DP1 a word. Q is zero if the line's FIRST WORD is that word - which ; is what lets a line be recognised without textSplit writing a zero into the middle of it, ; and a line that is being skipped must come through untouched. lineFirstIs: LDA.1 BRA lineFirstEnded LDB.0 XOR BNQ lineFirstNo INCD.0 INCD.1 BRI lineFirstIs lineFirstEnded: LDA.0 BRA lineFirstYes INIB 0x20 XOR BRQ lineFirstYes lineFirstNo: INIA 0x01 RSTB CCF ADD RET lineFirstYes: RSTA RSTB CCF ADD RET ; ---- A line inside a block that is not being taken ---- ; ; Only the three words that shape a block mean anything here, and they are matched WITHOUT ; the line being split or its names filled in: a line nobody is running must not be able to ; fail, and "$whatever" in a branch that was not taken is not a mistake. blockPassOver: SETD.0 CommandLine SETD.1 IfName CALL lineFirstIs BRQ ifSkipped SETD.0 CommandLine SETD.1 WhileName CALL lineFirstIs BRQ ifSkipped SETD.0 CommandLine SETD.1 ForName CALL lineFirstIs BRQ ifSkipped SETD.0 CommandLine SETD.1 ElseName CALL lineFirstIs BRQ doElse SETD.0 CommandLine SETD.1 EndName CALL lineFirstIs BRQ doEnd BRI prompt ; ---- set ---- ; ; "set name value" writes one down, "set name" gives it an empty value on purpose, and "set" ; on its own says what is written down already - because a name that fails loudly when it was ; never set needs somewhere to go and look. doSetVar: SETD.1 TextRest LDD.0.1 LDA.0 BRA setVarList ; Nothing after the word, so this is asking what is set. ; DP0 is still on the rest of the line - reading a byte through a pointer does not move it - ; so the name starts where it already points. SETD.1 SetVarWhich STD.0.1 CALL textSplit SETD.0 SetVarWhich LDD.0.0 SETD.1 TextRest LDD.1.1 CALL varSet BNQ setVarFull BRI prompt setVarFull: SETD.0 SetVarNoRoom CALL printString CALL newLine BRI commandFailed setVarList: RSTA SETD.1 VarSlot STA.1 setVarListNext: SETD.1 VarSlot LDA.1 INIB 0d8 CCF SUB BNC setVarListDone LDA.1 CALL varSlotAt LDA.3 BRA setVarListStep ; A free slot has nothing to say. PSHD.3 POPD.0 CALL printString SETD.0 SetVarIs CALL printString SETD.1 VarSlot LDA.1 CALL varSlotAt CALL varValueOf PSHD.3 POPD.0 CALL printString CALL newLine setVarListStep: SETD.1 VarSlot LDA.1 INCA STA.1 BRI setVarListNext setVarListDone: BRI prompt ; ---- 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: ; ---- The name as typed, and only then the name with ".sbx" on it ---- ; ; The exact word first is what makes a file reachable under the name it actually has: a ; script, a launcher, or a program somebody dropped the extension from. The word with the ; extension after it is what keeps every program already on a disk startable by the short ; name people have always typed for it. ; ; THE SUFFIX CAN ONLY EVER BE A SECOND GUESS. A file that is really there always wins over ; one that would have to be invented, so "notes.txt" stops sending the shell after a ; notes.txt.sbx that was never going to exist. RSTA SETD.0 NameSuffix STA.0 promptSearchName: CALL nameProgram SETD.0 NameOk LDA.0 BRA promptNextName ; 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 ; ---- Not a program is not the end of it ---- ; ; The file is there and it does not say SBEX. A script says what it is on its own first ; line, so the thing to do is read that rather than decide from the name that this could ; never have been runnable. THE SHELL RUNS WHAT IT READS, which is the whole point: the ; loader turns away anything that is not SBEX and scriptOpen turns away anything without ; the shebang, and between them nothing has to know what an extension means. INIB 0d4 CCF SUB BRQ promptAsScript ; 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 promptNextName: ; Nothing of that name in this place. The same place with the extension on, and then the ; next place. SETD.0 NameSuffix LDA.0 BNA promptElsewhere INCA STA.0 BRI promptSearchName promptAsScript: ; ---- The drive goes back before a script opens ---- ; ; The same handing back that a loaded program gets, and here it has to happen BEFORE the ; file is opened rather than after: a program is wholly in memory by then and a script is ; read a block at a time as it runs, so the lines in it must run on the disk the person was ; standing on. The name itself still says where the script lives, and reading it does not ; move anybody - see scriptKeepDrive. SETD.0 SearchDrive LDA.0 CALL sbfsUse ; Whatever followed the word, which reaches a script the same way it reaches a program. ; The line was split before the dispatch chain ran, so this is already the rest of it. SETD.1 TextRest LDD.2.1 SETD.1 ScriptArgsFrom STD.2.1 SETD.0 ProgramName CALL scriptOpen BRQ prompt ; It is open, and the next line read will come from it. ; Which of the two went wrong. There is a file, so "there is no such file" cannot happen. MVQA INIB 0x02 CCF SUB BRQ promptNotRunnable SETD.0 ScriptTooDeep BRI fileComplain promptNotRunnable: ; A file of that name IS there, and it is neither kind of runnable thing. Saying "I do not ; know" about it would send somebody looking for a file that is sitting right in front of ; them; what is wrong is the file, not the word. SETD.0 NotRunnable CALL printString SETD.0 CommandLine CALL printString CALL newLine BRI commandFailed 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 ; ---- And what is left of the disk ---- ; ; A listing that says what is there and not what is left says half of what anybody wants ; to know. SplitDisk has printed this since it was written; the machine's own listing had ; nothing to say about the disk it was listing. ; ; COUNTED RATHER THAN ASKED. The superblock keeps a free count, and sbfs.asm calls it "a ; note rather than the truth" in three separate places. The truth is what the entries add ; up to, and the walk above has just added them up - so this costs no disk read at all, ; where believing the note would cost one and be a guess. SplitDisk reads the note as well ; and says so when the two disagree, which is the right place for that check: the host tool ; is what you audit a disk with, and this is what you work on one with. ; ; ---- AND IT IS THE DISK, NOT THIS DIRECTORY ---- ; ; The first version of this added the blocks up as the listing walked past them, which cost ; no extra read and was wrong: that walk stops only on entries in the working directory, so ; the same disk was called 1,996 blocks free from the root and 2,025 from /Apps. Free space ; is a fact about the disk, so sbfsSpace reads the whole directory table for it - and the ; two implementations of this format disagreeing is what said so. CALL sbfsSpace BNQ dirNoSpace ; Entries first, because they are the ceiling nobody notices until they hit it - a disk of ; small files runs out of directory slots long before it runs out of blocks, and saying ; both means never having to work out which one is about to bite. SETD.0 DirEntries SETD.2 SbfsDirBlocks CALL sbfsSetWord INIA 0d3 SETD.0 DirDoubles STA.0 dirEntriesTimes: ; Eight entries to a directory block, which is three doublings on a machine with no ; multiply. Adding a number to itself is what a doubling is. SETD.0 DirEntries SETD.2 DirEntries CALL sbfsAddWord SETD.0 DirDoubles LDA.0 DECA STA.0 BNA dirEntriesTimes SETD.0 SbfsUsedEntries CALL printWordDecimal SETD.0 OfText CALL printString SETD.0 DirEntries CALL printWordDecimal SETD.0 EntriesText CALL printString ; What is left is the disk, less where the files begin, less what they hold. SETD.0 DirFree SETD.2 SbfsDiskBlocks CALL sbfsSetWord CALL sbfsFirstData SETD.0 DirFree SETD.2 SbfsCandidate CALL sbfsSubWord SETD.0 DirFree SETD.2 SbfsUsedBlocks CALL sbfsSubWord SETD.0 DirFree CALL printWordDecimal SETD.0 FreeText CALL printString CALL newLine ; ---- And how much of that is in one piece ---- ; ; Said only when it is not all of it, which is the common case and the quiet one. Files are ; laid down contiguously, so the free total is not what decides whether a file will fit - ; the longest run is. A disk with a thousand blocks free in ten pieces refuses a file of two ; hundred, and nothing in the listing would have hinted at it. ; ; A LISTING THAT SAID "longest run 1893" UNDER "1893 blocks free" WOULD BE NOISE on every ; healthy disk, and a line that only appears when something is wrong is a line somebody ; reads. It is also what makes the measurement worth its cost: a disk with one gap at the ; end, which is what an append-only disk is, walks the table twice and says nothing. CALL sbfsLargestRun BNQ dirNoSpace SETD.0 DirFree SETD.2 SbfsBiggest CALL sbfsCompareWord BRQ dirNoSpace SETD.0 RunText CALL printString SETD.0 SbfsBiggest CALL printWordDecimal CALL newLine dirNoSpace: 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. Which place to look in is ; NamePrefix, and whether to put ".sbx" on the end is NameSuffix - the search calls this once ; for each combination and the two answers are two different files to go and ask about. ; ; THE EXTENSION USED TO BE WHAT MADE A FILE REACHABLE BY NAME. It went on unconditionally, so ; typing "notes" looked for notes.sbx and typing "notes.txt" looked for notes.txt.sbx, and no ; text file, script or launcher could be started by typing what it is called. Now the word as ; typed is tried first and the extension is only a fallback for the programs that carry one. ; ; A word that already ends in ".sbx" is refused on the suffix pass rather than named twice: ; the exact pass has been there and looked, and asking the disk the same question again is a ; walk of a directory for an answer already known. ; ; 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 ; The word exactly as typed, which is the first thing asked for and the only thing asked ; for when the extension is not wanted. SETD.0 NameSuffix LDA.0 BRA nameMade ; 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. SETD.0 NameLeft LDB.0 INIA 0d55 CCF SUB BRC nameAppend PSHD.1 POPD.2 DPDN.2 0d4 SETD.0 SbxSuffix nameSuffixSame: LDA.0 BRA nameDone ; All of it matched, so the exact pass already asked for this. 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. ; ; AN EMPTY STRING, because prepending puts a separator in FRONT of whatever it is given - ; so being handed the separator itself wrote two of them and the root came out as "//". ; Nothing saw it for as long as the only caller was the prompt, which asks where it is ; only when that is not the root; the first caller that always asks found it at once. SETD.0 CwdAt LDD.1.0 LDA.1 BNA shellPathReady SETD.0 NoText 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 ; The file, and then whatever else was on the line. textSplit cuts the first word off in ; place and leaves TextRest on what follows, and a RET puts DP0 back - so afterwards DP0 ; names just the file and TextRest names what the script is being given. ; ; Through DP2, because DP0 is the file and must still be it when scriptOpen is called. CALL textSplit SETD.1 TextRest LDD.2.1 SETD.1 ScriptArgsFrom STD.2.1 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 ; ---- Every voice let go of ---- ; ; The same argument as the screen, and for a stronger reason. A program that took the sound ; device and stopped has nothing left that could end a note it was holding: a gate is a ; register on the device and only a program can drop one. A held note sustains until somebody ; says otherwise, so what this bounds is the difference between a machine that rings for a ; moment and one that rings until it is switched off. ; ; IT DOES NOT MAKE THE DEVICE SILENT AT ONCE, and does not pretend to. Dropping a gate ; RELEASES a note rather than stopping it, so whatever release the patch was given still runs. ; A bounded tail rather than an endless one is the part the system can be responsible for ; without knowing what instrument the program had built. ; ; A PROGRAM THAT FAULTS CANNOT TIDY UP AFTER ITSELF, which is why this is called from the ; fault path too. A machine that will not stop humming is a poor place to read an error ; message, and the program that would have quietened it is exactly the one that just died. ; ; Safe from a fault handler: four port writes, no service, no disk, and nothing that can ; refuse. soundQuiet: RSTA soundQuietOne: OUTA 0x41 ; This channel. Every sound port writes to whichever was last. PSHA RSTA OUTA 0x45 ; Let go of whatever it was holding. POPA INCA INIB 0d4 CCF SUB BNQ soundQuietOne 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. OUTA 0x3C ; And the screen everybody else's map is in. ; ---- And no window left over the shell ---- ; ; A window is a layer at a screen position that does not scroll, which is exactly what makes ; one left behind so unpleasant: it sits over the top rows of whatever comes next and cannot ; be scrolled off, cleared away or typed past. Lunar Porter left its fuel gauge there and ; the shell came back with FUEL across the top of it and the cursor underneath. ; ; Taken away rather than given back, for the same reason the sprite table is: nothing the ; shell draws is a window, so there is nothing to restore - and a program that FAULTED while ; one was up could not have taken it down itself. OUTA 0x3D ; No rows, which is no window. OUTA 0x3E ; And nowhere for it to start, so the state is a known one. ; ---- Which of the two screens is being shown ---- ; ; A program that draws into the back buffer and flips is showing screen one, and the shell ; knows nothing about that. Its own scrollback, its prompt and every line the person typed ; are in screen NOUGHT, so a program that exited while flipped would hand back a shell ; drawing correctly onto a screen nobody had ever written to - blank, and looking for all ; the world like the machine had lost everything. ; ; Put back rather than left, by the same rule as the cursor and the ink in handleExit: a ; program that stopped early is not around to put anything back, and the shell owns what ; the person is looking at. ; ---- 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 ; ---- And no sprites left standing over the shell ---- ; ; The table is in the atlas at 0xC000, and the screen save does not cover it: the pages it ; walks run to the end of the map and pick up again at the palette, with the sprites in the ; gap between. That is deliberate rather than an oversight. Nothing the shell draws is a ; sprite, so there is nothing to GIVE BACK - what is needed is to take away, or a program ; that put something on the screen and left would have left it sitting over the prompt, ; still there, still in front of everything, and nothing able to type it away. ; ; A size of nought is no sprite, so filling the table with nought is all of it, and it is ; one command rather than 256 of them. CALL screenBank INIA 0d4 OUTA 0xE3 INIA 0xC0 OUTA 0xE4 RSTA OUTA 0xE5 OUTA 0xE2 ; Fill takes the byte it writes from SourceLow. INIA 0x10 OUTA 0xE6 RSTA OUTA 0xE7 ; 0x1000, which is 256 entries of sixteen bytes. INIA 0x02 OUTA 0xE8 RET ; Video memory as banks 4 and 5, 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. ; ; The BACK BUFFER is not given a number here. The system saves and restores what is on the ; screen, and what is on the screen is screen nought - a program that wants the other one ; registers it itself, the same as any program wanting memory nobody else is using. screenBank: INIA 0d4 OUTA 0xE3 INIA 0x30 OUTA 0xE2 INIA 0x03 OUTA 0xE8 ; Four is the atlas: the tiles and the palette. INIA 0d5 OUTA 0xE3 INIA 0x3A OUTA 0xE2 INIA 0x03 OUTA 0xE8 ; Five is the screen: the map, or a bitmap. 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 ; ---- And which of the screen's two banks that page is in ---- ; ; The tiles and the palette are the atlas and the map is the screen, so the PAGE NUMBER ; ALREADY SAYS WHICH - 0x00 to 0x3F and 0xFC to 0xFF one way, 0x40 to 0xBF the other. Nothing ; has to be remembered alongside the page, and there is no second list to keep in step with ; screenPageFor above. ; ; IN Q AND NOT A, for the same reason screenPageFor answers there: RET puts A back the way it ; found it, so a subroutine that answered in A would answer with the argument it was given. screenBankFor: INIB 0x40 CCF SUB BRC screenBankAtlas ; Borrowed, so it is below 0x40 and is a tile. INIB 0xC0 CCF SUB BRC screenBankScreen ; Borrowed, so it is below 0xC0 and is the map. screenBankAtlas: INIA 0d4 RSTB CCF ADD RET screenBankScreen: INIA 0d5 RSTB CCF ADD RET ; A holds a page of video memory. Its 256 bytes come into ScreenBlock. screenFromVideo: CALL screenBankFor ; A is the page and RET hands it back; Q comes back the bank. PSHA ; The page, which the bank is about to want A for. MVQA 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: CALL screenBankFor PSHA MVQA 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 ; And where the program itself came from, so it can be asked later. CALL runWhere 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 ; ---- Where the program itself came from ---- ; ; ProgramName holds the path the search settled on, and that path may be a bare name: a ; program found where somebody was standing is named by the word that was typed. A BARE NAME ; MEANS THE WORKING DIRECTORY, and a program is entitled to move out of it - so a program ; asked later where it lives would be told "beside wherever you are now", which is the one ; answer that is certainly wrong. ; ; So it is made absolute HERE, once. Where a program came from is a fact about its start and ; cannot change afterwards; where the person is can change on the program's own next line. runWhere: ; A path that begins with a separator has already said where it is. SETD.0 ProgramName LDA.0 INIB 0x2F CCF SUB BRQ runWhereAsFound ; Or a drive in front of it, which says so twice over. A digit and then a colon, both, the ; same test the path walker makes - a name may begin with a digit, and the colon is the ; whole of what tells the two apart. LDA.0 INIB 0x30 CCF SUB BRC runWhereRelative ; Below a digit. LDA.0 INIB 0x3A CCF SUB BNC runWhereRelative ; Above one. INCD.0 LDA.0 INIB 0x3A ; splitlint[redundant-assignment]: 0x3A above is one past '9' and here it is ':' CCF SUB BRQ runWhereAsFound runWhereRelative: ; Where the machine is, written out. shellPath builds it backwards from the end of its ; buffer and hands back a pointer into the middle, which is a string like any other. CALL shellPath SETD.0 CwdAt LDD.0.0 SETD.1 RunPath INIB 0d127 CALL copyText ; The end of it, and the last character on the way past. At the root there is no last ; character at all, which is an answer rather than a special case: nought is not a ; separator, so a separator gets written and the path begins with one. SETD.0 RunPath RSTA SETD.1 RunPathLast STA.1 runWhereEnd: LDA.0 BRA runWhereJoin SETD.1 RunPathLast STA.1 INCD.0 BRI runWhereEnd runWhereJoin: SETD.1 RunPathLast LDA.1 INIB 0x2F CCF SUB BRQ runWhereName ; It ends in a separator already, so a second would be a lie. INIA 0x2F STA.0 INCD.0 runWhereName: ; And the program's own name under it. DP0 is where it goes, which is at most 127 bytes ; along a buffer of 192 - so the 64 a name can be always fits. PSHD.0 POPD.1 SETD.0 ProgramName INIB 0d64 CALL copyText RET runWhereAsFound: SETD.0 ProgramName SETD.1 RunPath INIB 0d192 CALL copyText RET 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 same bargain for the other thing a program is entitled to know about its own start: ; where it came from. Worked out in runWhere before the program was given the machine, so ; this only has to hand it over. handleWhereAmI: PSHD.0 POPD.1 SETD.0 RunPath 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. ; ---- Walking a directory on a program's behalf ---- ; ; The one thing the filesystem could do that no program could ask for. dir has always been ; able to list because it lives in here; a loaded program had no way to find out what names ; exist at all, which left a file manager, a backup, and the package manager still to come ; all unable to be written. ; ; Two entry points into one routine, because the difference between them is a single call. ; ; Q ANSWERS THE KIND, so one value says both whether there is an entry and what it is: 0 a ; file, 1 a directory, 2 a save that stopped before it committed, 0xFF nothing more. A ; caller that only wants names tests for 0xFF and ignores the rest. handleDirFirst: SETD.2 DiskReady LDA.2 BRA dirWalkNoMore CALL sbfsFirst BRI dirWalkGot handleDirNext: SETD.2 DiskReady LDA.2 BRA dirWalkNoMore CALL sbfsNext dirWalkGot: BNQ dirWalkNoMore ; The name first, because working out the kind wants the registers. B is still the room ; the caller asked for: a CALL puts it back, and nothing above has taken it. PSHD.0 POPD.1 SETD.0 SbfsName CALL copyText ; ---- What kind of thing it is ---- ; ; Asked in the same order dir asks it. An unfinished save is looked at FIRST because it is ; the one thing here that is not really a file yet, and it is SHOWN rather than hidden: ; the bytes are all there under that name, so a program that can see it is a program that ; can rename it back. SETD.2 SbfsFoundFlags LDA.2 INIB 0x04 AND BNQ dirWalkUnfinished SETD.2 SbfsFoundFlags LDA.2 INIB 0x02 AND BNQ dirWalkDirectory RSTA BRI dirWalkAnswer dirWalkDirectory: INIA 0d1 BRI dirWalkAnswer dirWalkUnfinished: INIA 0d2 BRI dirWalkAnswer dirWalkNoMore: ; A machine with no disk has nothing to walk, and says the same thing as a walk that has ; run out. There is no third answer worth telling apart: a caller that cannot list is a ; caller that lists nothing either way. INIA 0xFF dirWalkAnswer: RSTB CCF ADD ; A is the answer, so Q becomes it. SRET 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 ; And every voice let go of, for the same reason the screen is: a program that has stopped ; cannot drop a gate it left up. CALL soundQuiet ; 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: " ; A file of that name is there and it is neither a program nor a script, which is a different ; thing from a word this shell does not know and reads differently to whoever typed it. NotRunnable: "that is not a program or a script: " 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: ", " OfText: " of " EntriesText: " entries, " FreeText: " blocks free" RunText: "the longest run is " 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 [words] 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. It is a convention now rather than a rule: the ; shell tries the word as typed first and only puts these four characters on afterwards. 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: " ? " 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" ; ---- The shell's own words, packed ---- ; ; Fifteen names with nothing between them, which is a TABLE and not an accident of layout. ; Each is a string ending in a zero, so the next one begins after that zero and walking them ; needs no pointers and no lengths - which is what makes completing a half typed command ; possible at all, since the dispatch below is a chain of comparisons and cannot be walked. ; ; NOTHING MAY BE PUT BETWEEN THEM. Tests/docs.sh checks that this run holds exactly the names ; the dispatch tests and that the count below agrees, so a command added without a name here ; is caught rather than silently left out of what Tab knows about. ; ; AND EACH LABEL ENDS IN "Name", which is how that check recognises one. A label called ; ForName2 was not counted, and the check said the run was one shorter than the count claimed ; rather than saying why - which is the right way round for it to fail, but the convention is ; worth writing down where the names are rather than only in the checker. ShellNames: 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" MonitorName: "monitor" SetVarName: "set" IfName: "if" ElseName: "else" EndName: "end" SameName: "same" WhileName: "while" ForName: "for" ; How many of them, since a run of strings does not say where it stops. ShellNameCount: 0d22 AppsPath: "/Apps" ArgsWord: "args" ; An empty string, for a script the machine started rather than somebody typing, which was ; therefore given nothing. NoText: 0x00 VarNoSuch: "nothing is set called " VarTooLong: "that line is too long once its names are filled in" VarNameLong: "that name is longer than a name can be" SetVarNoRoom: "there is no room for another name" SetVarIs: " is " IfUsage: "if wants a command to decide by" IfTooDeep: "that is more blocks than will fit inside one another" ElseLonely: "else with no if above it" EndLonely: "end with no if above it" LoopTyped: "a loop wants a script to go back through" ForUsage: "for wants a name, then in, then the words" InName: "in" ; 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 ; What the entries add up to, what is left, and how many slots there are to fill. Counted on ; the way past rather than asked of the superblock, whose count sbfs.asm calls a note. DirFree: 0x00 0x00 DirEntries: 0x00 0x00 DirDoubles: 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 ; Nought for the word exactly as typed, one for the word with ".sbx" put on the end. NameSuffix: 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 ; ---- Where the program came from, made absolute ---- ; ; A hundred and ninety two, which is a working directory of up to 127, a separator, and a ; name of up to 64. None of those three can be made longer without the others being counted ; again. RunPath: #Reserve 0d192 RunPathLast: 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 ; And where it is when the shell is between lines, which is not the same thing. See prompt. ShellStack: 0x00 0x00 ; ---- Which blocks are open, and whether their lines are being run ---- BlockStack: #Reserve 0d128 BlockDepth: 0x00 BlockHold: 0x00 ; What kind of block is about to be pushed, since blockPush takes the state in A and there is ; nowhere else for a second answer to go. LoopKind: 0x00 ; Set by an end that is sending a for round again, and read by the for line it lands on. LoopResume: 0x00 ForUsed: 0x00 ForVarAt: 0x00 0x00 ForWordAt: 0x00 0x00 ; Set while the line being run is an "if" condition rather than somebody's own command. IfPending: 0x00 SameFirst: 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 Tab is working on ---- TabWordAt: 0x00 TabWordLen: 0x00 TabCount: 0x00 TabLeft: 0x00 TabSeen: 0x00 TabAt: 0x00 TabHold: 0x00 TabPut: 0x00 TabNarrowAt: 0x00 TabBestLen: 0x00 ; Where a candidate is, put down so that a routine which needs to walk it can pick it up ; without moving the pointer its caller is still using. TabPointer: 0x00 0x00 ; And where the candidate being looked at begins, since the comparison walks past it. TabStart: 0x00 0x00 ; The answer so far: the first match, cut back by every one after it to where they agree. TabBest: #Reserve 0d64 ; The word being matched, as somebody would type it: a name with its extension taken off, or ; a directory with a separator put on. TabCandidate: #Reserve 0d48 ; The part of the word up to and including its last separator, which is the directory to look ; in. Empty means where the machine already is. TabDir: #Reserve 0d64 TabDirLen: 0x00 TabCopyAt: 0x00 TabFirst: 0x00 ; Whether the walk is looking for the answer or showing what the answers were. TabShowing: 0x00 ; Where the machine was standing before a directory was walked on its behalf. TabWasCwd: 0x00 0x00 TabWasDrive: 0x00 ; ---- What a name stands for ---- ; ; Eight records of sixty four bytes: sixteen of name, then forty eight of value. VarSlots: #Reserve 0d512 VarSlot: 0x00 ; One word of what a script was given, copied out so that it ends in a zero like every other ; value. A whole line of room, because one word can be the whole line. VarWord: #Reserve 0d128 VarWordWanted: 0x00 VarWanted: 0x00 0x00 VarKeepValue: 0x00 0x00 VarKeepLine: 0x00 0x00 VarRoom: 0x00 VarHold: 0x00 ; The line with its names filled in, built here and copied back when it is whole. VarLine: #Reserve 0d128 VarPut: 0x00 0x00 VarUsed: 0x00 VarName: #Reserve 0d16 VarNameLen: 0x00 SetVarWhich: 0x00 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 osWhereAmI handleWhereAmI osDirFirst handleDirFirst osDirNext handleDirNext osBootState handleBootState osBootSettle handleBootSettle Device 0x20 diskDone