Files
SplitBit-Emulator/Programs/CosmOS/Source/cosmos.asm
T
AnachronautandClaude Opus 5 023362b05a A second screen, and one port to say which is shown
A screen drawn where it can be seen is seen half drawn. A program that
moves forty things and rewrites the map underneath them is wrong for as
long as it takes to put them all right, and at a megahertz that is long
enough to look at.

So the device brings a second screen bank, on port 0x3B, and port 0x3C
says which of the two is displayed. Everything a program draws into the
other one is invisible until one byte shows the whole of it at once.

ONE REGISTER IS ENOUGH, where the hardware this imitates needed two. The
other said which screen the CPU's window pointed at; there is no window
here, because a program reaches a bank through the memory controller by
its number. Writing to the screen that is not shown is a matter of naming
its bank, and the device never has to be told.

And a flip cannot tear: a frame is drawn from one bank in one go, so a
flip either happened before that frame or happens before the next. There
is nothing to race, where the real machines had to catch the few lines
between frames to swap in.

The console draws into whichever screen is displayed rather than one of
its own, so a fault message lands where somebody can read it even if a
game had flipped. And CosmOS puts the displayed screen back at exit, the
way it already puts back the cursor and the ink: a program that faulted
while flipped could not have, and a shell that only came out right for
programs which remembered would come out wrong the day one crashed.

Flip.asm is the worked example. It deliberately does NOT restore the
display itself - that is the point of the paragraph above, and it is what
makes the system's guarantee the thing under test rather than the
program's good manners. Written the other way round first, where it
passed with the guarantee deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 10:31:29 -04:00

8217 lines
195 KiB
NASM

; cosmos.asm
; CosmOS, and the shell that is most of it.
;
; The machine boots into this. It registers what the hardware brought, mounts whatever
; disk is attached, and then reads lines and does what they say until there is no more
; typing to be had.
;
; ---- Where things live ----
;
; The system keeps to the bottom of both memories, and everything above is for whatever
; it is running:
;
; Program Memory 0x0000 - 0x4FFF the system
; 0x5000 - a loaded program's code
; Data Memory 0x0000 - 0x2FFF the system
; 0x3000 - a loaded program's data
;
; Nothing enforces that. Nothing can: the fence guards a range, and this is a convention
; about which range belongs to whom rather than a rule about what may be touched. The
; assembler prints both segment sizes, and they are what to watch.
;
; A program is staged at 0x8000 while it is being loaded, which is inside the region a
; loaded program will own. That is safe because nothing is running during a load, and it
; is where a big program can be read without the system reserving the room for good.
;
; ---- What it can do ----
;
; dir List what is in the current directory.
; cd Go somewhere else, or to the root when told nothing.
; mkdir Make a directory. rmdir Remove an empty one.
; delete Remove a file. rename Give one another name.
; load Read a program off the disk and put it where it asks to go.
; run Start the program that was loaded.
; help Say what these are.
; exit Stop, or leave the monitor if that is where you are.
;
; Anything else that is not one of those is looked for as a program and run if it is
; found, so most of what the machine does is not in this list at all.
;
; The dispatch below is a chain of comparisons, which is the right shape for five commands
; and the wrong shape for twenty; it is eleven now, and when it grows the table that
; dispatchTest.asm demonstrates is where it should go.
;
; Written by Anachronaut
#Include console.asm
#Include text.asm
#Include sbfs.asm
#Include services.asm
#Include script.asm
#Program
boot:
; ---- The screen this system wants ----
;
; Eighty columns, because that is what CosmOS was written for: its own help text is
; seventy-four characters wide, and dir, the monitor and the assembler's messages all
; assume room. A machine wakes up in the smaller mode, which is right for a machine - it
; is the system that knows what shape of screen its own output needs.
;
; Harmless where there is no screen. Writing to a port nothing answers on does nothing,
; so this is one instruction wasted on a machine with a terminal instead.
INIA 0x01
OUTA 0x31
; And a cursor, so that a person can see where the next thing they type will go. A machine
; wakes up without one, which is right: a program painting its own screen does not want one
; blinking in the middle of it. A system that reads lines from a person does.
INIA 0x04
OUTA 0x02
SETD.0 Banner
CALL printString
CALL newLine
; Find out whether there is a filesystem to talk to. Doing this once at boot rather than
; once per command means a disk swapped underneath us is not noticed, which is honest
; for a machine whose disk is a file named on the command line.
CALL sbfsMountAll
SETD.0 DiskReady
BNQ bootNoDisk
INIA 0x01
STA.0
; ---- Saying that this start arrived ----
;
; The loader marks the disk before handing over, and nothing clears it but this. A system
; that crashes on the way here leaves the mark, and the loader finding it still set next
; time is how a machine that will not start says so.
;
; HERE RATHER THAN LATER, and the threshold is the whole of what the mark means. It is
; not a claim that anything works - a prompt can be reached by something broken in every
; other way. It is the point where somebody can type, which is what the fallback exists
; to give back: anything wrong past here can be fixed from the prompt, and nothing wrong
; before it can be fixed at all.
CALL sbfsBootState
BNQ bootStartup
SETD.0 SbfsStateWas
LDA.0
INIB 0d1
XOR
BRQ bootArrived
INIB 0d2
XOR
BNQ bootStartup
; Started by the fallback, so the system somebody asked for is not the one running. Said
; once, here, because there is nowhere else it would be noticed.
SETD.0 OnFallback
CALL printString
BRI bootStartup
bootArrived:
RSTA
CALL sbfsSetBootState
BRI bootStartup
bootNoDisk:
RSTA
STA.0
SETD.0 NoDisk
CALL printString
CALL newLine
; ---- Something to run before anybody types ----
;
; Every way of arriving at the prompt for the first time comes through here, which is the
; point: a startup script should run whether the disk was marked, unmarked, or is not there
; at all - and in the last case scriptOpen simply finds nothing.
;
; A MISSING ONE IS NOT A FAULT and says nothing, because a clean install has none and a
; machine that complained at every boot about a file nobody wrote would be teaching its owner
; to ignore it. A file that is THERE and is not a script is the other case entirely: somebody
; meant it to run.
bootStartup:
SETD.0 StartupName
CALL scriptOpen
BRQ bootReady
MVQA
INIB 0x01
CCF
SUB
BRQ bootReady ; There is none, which is ordinary.
SETD.0 StartupNotOne
CALL printString
CALL newLine
bootReady:
; ---- 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
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: programs at the front of a line, since that is all a first word can
; usefully be, and anything at all after it.
SETD.1 TabFirst
LDA.1
SETD.1 TabPrograms
STA.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
BRI tabEntryOffer
tabEntryFile:
; A file. When the first word of a line is being finished it can only usefully be a
; program, and the name to type for one is the name without its extension - so anything
; else is not offered at all rather than offered and useless.
SETD.1 TabPrograms
LDA.1
BRA tabEntryOffer
CALL tabStripSbx
BNQ tabEntryDone
tabEntryOffer:
SETD.3 TabCandidate
CALL tabOffer
tabEntryDone:
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
; Takes ".sbx" off the end of TabCandidate. Q is zero if it was there, and one if it was not
; - which is how a file that is not a program is passed over.
tabStripSbx:
; How long the name is, counted rather than measured from a pointer, because what is
; wanted next is a NUMBER: four characters back from the end is only a place if there are
; four of them to go back over.
SETD.0 TabCandidate
RSTB
tabStripCount:
LDA.0
BRA tabStripCounted
INCD.0
INCB
BRI tabStripCount
tabStripCounted:
; The count is in B and wanted in A, and there is no instruction that moves one to the
; other: adding it to nothing is how a value crosses from B to A on this machine.
RSTA
CCF
ADD
MVQA
INIB 0d4
CCF
SUB
BRC tabStripNo ; Shorter than the extension, so it cannot end in it.
MVQA
SETD.3 TabCandidate
DPUA.3 ; Four from the end, which is where the extension would begin.
SETD.0 SbxText
tabStripMatch:
LDA.0
BRA tabStripYes
LDB.3
XOR
BNQ tabStripNo
INCD.0
INCD.3
BRI tabStripMatch
tabStripYes:
; It ended in the extension, so the name to type is what comes before it. DP3 was left on
; the zero by the comparison, so four back is where the dot was.
INIA 0d4
DPDA.3
RSTA
STA.3
RSTB
CCF
ADD ; Q is zero: it is a program.
RET
tabStripNo:
INIA 0x01
RSTB
CCF
ADD ; Q is one: it is not.
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.
SETD.0 VarName
CALL varFind
BNQ varExpandNoSuch
CALL varValueOf
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
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
lineTrimDone:
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:
CALL nameProgram
SETD.0 NameOk
LDA.0
BRA promptElsewhere ; Too long to be a path, so it is not the name of one.
CALL loadProgram
SETD.0 LoadStatus
LDA.0
BNA promptNotLoaded
; ---- Loaded, so the drive goes back to the person ----
;
; The program is in memory now and the block numbers it came from mean nothing any more,
; which is what makes this safe here and not in the middle of a path. A program fetched
; from the system disk then runs on the disk its user was standing on - which is the whole
; point of being able to keep a disk of your own.
SETD.0 SearchDrive
LDA.0
CALL sbfsUse
BRI runLoaded ; It loaded, and the machine is its now.
promptNotLoaded:
SETD.0 LoadStatus
LDA.0
; No file of that name is not a fault. It is the ordinary case of a word this shell does
; not know, and it is the only answer worth looking somewhere else for. Anything else
; means a file of that name IS there and something is wrong with it, and answering
; "I do not know: Snake" about a Snake.sbx that is sitting on the disk would send
; somebody looking in the wrong place.
INIB 0d2
CCF
SUB
BNQ loadFailed
promptElsewhere:
; Was that the last place there is?
SETD.0 NamePrefix
LDA.0
INIB 0d2
CCF
SUB
BRQ promptGaveUp
; A word beginning with a separator has said where to look, and looking somewhere else
; would be answering a different question from the one asked.
SETD.0 CommandLine
LDA.0
INIB 0x2F
CCF
SUB
BRQ promptSayUnknown
SETD.0 NamePrefix
LDA.0
INCA
STA.0
BRI promptSearch
promptGaveUp:
; Every place has been tried, and the drive is put back before anybody is told anything:
; the search moved it, and a word the shell does not know should not move somebody either.
SETD.0 SearchDrive
LDA.0
CALL sbfsUse
promptSayUnknown:
; Saying which word was not understood is worth the four instructions: it tells somebody
; who mistyped what they actually typed.
SETD.0 Unknown
CALL printString
SETD.0 CommandLine
CALL printString
CALL newLine
BRI commandFailed
; Running out of console leaves the cursor part way along a line, because there was no
; return at the end to move it on. Somebody who typed "exit" has already pressed one, and
; a second would only leave a blank line behind.
; Leaving whatever you are in: the monitor if you are in it, and the machine if you are
; not. Two exits to stop from the monitor, which is what every nested prompt has ever asked
; for and reads the right way round.
doExit:
SETD.0 Mode
LDA.0
BRA quit
RSTA
STA.0
BRI prompt
doMonitor:
INIA 0x01
SETD.0 Mode
STA.0
SETD.0 MonitorHelp
CALL printString
CALL newLine
BRI prompt
quitRanOut:
CALL newLine
quit:
SETD.0 Farewell
CALL printString
CALL newLine
HALT
; ---- dir ----
;
; Walks the directory and prints what is in it. A free entry in the middle of a directory
; is stepped over by the walk, so what comes out is the files and nothing else.
doDir:
SETD.0 DiskReady
LDA.0
BRA dirNoDisk
RSTA
SETD.0 DirSeen
STA.0
SETD.0 DirFolders
STA.0
CALL sbfsFirst
BRI dirCheck
dirStep:
CALL sbfsNext
dirCheck:
BNQ dirDone
SETD.0 DirSeen
LDA.0
INCA
STA.0
SETD.0 SbfsName
CALL printString
SETD.0 SbfsName
CALL nameWidth
MVQA
CALL printSpaces
; A save that stopped before it committed says so instead of saying a size, because the
; size it holds is the room it asked for rather than what was written into it. Asked
; first, since it is the one thing here that is not really a file yet.
;
; IT IS SHOWN RATHER THAN HIDDEN, and that is the whole of the recovery this format
; offers: the bytes are all there under that name, so seeing it is what lets somebody
; rename it back. Left off the listing it would be blocks nobody could account for.
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x04
AND
BNQ dirUnfinished
; A directory says so instead of saying a size. It has no blocks, so the arithmetic
; below would call it a file of no bytes - which is a different thing that happens to
; look the same from here.
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ dirIsDirectory
; A file's length is its block count times 256 plus its tail, which is the block count
; in the high byte and the tail in the low one. Nothing has to multiply anything.
;
; THAT ONLY WORKS WHILE THE BLOCK COUNT FITS IN A BYTE. Two hundred and fifty six blocks
; is sixty five thousand five hundred and thirty six bytes, and the number that comes out
; of the shift is sixteen bits wide - so a file of that size or more came out as itself
; less 65536, which is a plausible number and a wrong one. cosmos.asm is 82,996 bytes and
; this called it 17,460.
;
; Such a file says its size in BLOCKS instead. Printing the true figure would want
; decimal printing twenty four bits wide, which is a page of console.asm to say something
; nobody reads more precisely than "big"; changing the unit says it exactly and can never
; be wrong.
SETD.0 SbfsFileBlocks
LDA.0
BNA dirInBlocks
SETD.0 SbfsFileBlocks
INCD.0
LDA.0
SETD.1 DirSize
STA.1
SETD.0 SbfsFileTail
LDA.0
INCD.1
STA.1
SETD.0 DirSize
CALL printWordDecimal
CALL newLine
BRI dirStep
dirInBlocks:
SETD.0 SbfsFileBlocks
CALL printWordDecimal
SETD.0 BlocksText
CALL printString
CALL newLine
BRI dirStep
dirUnfinished:
SETD.0 UnfinishedText
CALL printString
CALL newLine
BRI dirStep
dirIsDirectory:
SETD.0 DirFolders
LDA.0
INCA
STA.0
SETD.0 DirectoryText
CALL printString
CALL newLine
BRI dirStep
dirDone:
; Directories were counted alongside the files and now come back out of the total, so
; that "three files" means three files. Saying it any other way makes the number
; disagree with the listing right above it, which is the sort of thing that teaches
; somebody not to trust the listing.
SETD.0 DirFolders
LDA.0
SETD.1 DirTaken
STA.1
SETD.0 DirSeen
LDA.0
LDB.1
CCF
SUB
MVQA
STA.0
SETD.0 DirSeen
LDA.0
CALL printByteDecimal
; One file is not one files. Cheap to get right and it reads as carelessness otherwise.
SETD.0 DirSeen
LDA.0
DECA
BRA dirOne
SETD.0 FilesText
BRI dirCount
dirOne:
SETD.0 FileText
dirCount:
CALL printString
; And how many of them were directories, but only when there were any. A disk with none
; on it should read exactly the way it always did.
SETD.0 DirFolders
LDA.0
BRA dirNoFolders
SETD.0 AndText
CALL printString
SETD.0 DirFolders
LDA.0
CALL printByteDecimal
SETD.0 DirFolders
LDA.0
DECA
BRA dirOneFolder
SETD.0 FoldersText
BRI dirFolderCount
dirOneFolder:
SETD.0 FolderText
dirFolderCount:
CALL printString
dirNoFolders:
CALL newLine
BRI prompt
dirNoDisk:
SETD.0 NoDisk
CALL printString
CALL newLine
BRI commandFailed
; DP0 names a string. Q is how many spaces pad it out to twenty four columns. A name
; already that long gets one space, so that it cannot run into the number after it.
nameWidth:
INIA 0d24
SETD.1 WidthLeft
STA.1
widthLoop:
LDA.0
BRA widthDone
SETD.1 WidthLeft
LDA.1
DECA
STA.1
BRA widthFloor
INCD.0
BRI widthLoop
widthFloor:
INIA 0d1
SETD.1 WidthLeft
STA.1
widthDone:
SETD.1 WidthLeft
LDA.1
RSTB
CCF
ADD
RET
; ---- Making a file name out of a typed word ----
;
; Turns what was typed into the name of a file to go and look for. ".sbx" goes on the end
; unless it is already there, so that "Snake" and "Snake.sbx" both find the same file.
;
; THE EXTENSION IS WHAT MAKES A FILE REACHABLE BY NAME. Typing "notes" looks for notes.sbx
; and typing "notes.txt" looks for notes.txt.sbx, so a text file cannot be started by
; typing what it is called, whatever is inside it. Only load reaches a file by its whole
; name, which is why the monitor can still put any file at all in front of itself.
;
; NameOk is one if there is a path in ProgramName and zero if the word could not be made
; into one. What limits it is the buffer, not the format: each NAME along a path is still
; twenty two characters, and the path walker refuses a longer one rather than cutting it
; down. Being refused here reads as an unknown command, which is the truth: nothing this
; shell can reach is called that.
nameProgram:
RSTA
SETD.0 NameOk
STA.0 ; Not a name until it turns out to be one.
SETD.1 ProgramName
INIB 0d59 ; What is left of the buffer, less the four for the extension.
; The system's own place for programs goes on the front, when that is what is being
; tried. Putting it here rather than pasting it on afterwards is what keeps the ".sbx"
; test below looking at the end of the whole thing.
SETD.0 NamePrefix
LDA.0
BRA nameFromLine
; One is the system's place on this disk; two is the same place on drive 0.
INIB 0d2
CCF
SUB
BRQ nameSystemApps
SETD.0 AppsPrefix
BRI namePrefixCopy
nameSystemApps:
SETD.0 SystemAppsPrefix
namePrefixCopy:
LDA.0
BRA nameFromLine
STA.1
INCD.0
INCD.1
DECB
BRI namePrefixCopy
nameFromLine:
SETD.0 CommandLine
nameCopy:
LDA.0
BRA nameCopied
STA.1
INCD.0
INCD.1
DECB
BNB nameCopy
RET ; Longer than the buffer holds, so it is not a path either.
nameCopied:
; DP1 is on the byte after the word, which is where an extension would go, and B is what
; is left. B is written down before anything else wants the registers.
RSTA
STA.1
SETD.0 NameLeft
STB.0
; Only a word of four characters or more can already end in ".sbx". Stepping back four to
; look at a shorter one would read whatever happens to sit in front of the buffer.
INIA 0d55
CCF
SUB
BRC nameAppend
PSHD.1
POPD.2
DPDN.2 0d4
SETD.0 SbxSuffix
nameSuffixSame:
LDA.0
BRA nameMade ; The suffix ran out with all of it matched.
LDB.2
CCF
SUB
BNQ nameAppend
INCD.0
INCD.2
BRI nameSuffixSame
nameAppend:
SETD.0 NameLeft
LDA.0
INIB 0d4
CCF
SUB
BRC nameDone ; Not four characters of room, so there is no name to be made.
; The suffix carries its own zero, so copying it to the end of the word ends the word.
SETD.0 SbxSuffix
nameSuffixCopy:
LDA.0
STA.1
BRA nameMade
INCD.0
INCD.1
BRI nameSuffixCopy
nameMade:
INIA 0x01
SETD.0 NameOk
STA.0
nameDone:
RET
; ---- load ----
;
; Reads a program off the disk and puts it where its header asks to go. Nothing relocates
; anything: the addresses in the header are the ones the program was built for, and it
; would not work anywhere else.
;
; The whole file is staged at 0x8000 first and then blitted into place, because where the
; pieces belong is not known until the header has been read, and the header is in the file.
;
; The command is a thin thing over loadProgram, which is a subroutine because typing a
; program's name loads it too and neither caller should own the loading. What differs
; between them is only what a fault means: "load Snake.sbx" on a disk without it has been
; given a wrong name, and "Snake" on the same disk has typed a word this shell does not
; know.
doLoad:
SETD.1 TextRest
LDD.0.1
LDA.0
BRA loadNothingNamed
; The path exactly as typed. load is how a file is reached by its whole name, so nothing
; is added to it and nothing is assumed about what it ends in.
SETD.1 ProgramName
INIB 0d64
CALL copyText
CALL loadProgram
SETD.0 LoadStatus
LDA.0
BNA loadFailed
SETD.0 LoadedText
CALL printString
SETD.0 LoadedEntry
CALL printWordHex
CALL newLine
BRI prompt
loadFailed:
SETD.1 LoadMessage
LDD.0.1
CALL printString
CALL newLine
BRI commandFailed
loadNothingNamed:
SETD.0 LoadWhat
CALL printString
CALL newLine
BRI commandFailed
; ---- loadProgram ----
;
; The name is in ProgramName. LoadStatus says what happened and LoadMessage names the text
; for it, because a subroutine cannot hand anything back in a register that a RET puts
; back, and here there are two things to hand back:
;
; 0 loaded, and LoadedEntry says where it starts
; 1 no filesystem on the disk
; 2 no file of that name
; 3 the disk would not read it
; 4 it is not a program
; 5 a version of the format this loader does not know
; 6 more vectors than there is room to keep
; 7 it is a directory
;
; Two is the one worth telling apart from the others. It is the only outcome where nothing
; was wrong with the disk or with a file, and so the only one a caller can fairly report as
; something other than a fault.
loadProgram:
RSTA
SETD.0 LoadStatus
STA.0
SETD.0 DiskReady
LDA.0
BRA loadNoDisk
SETD.0 ProgramName
CALL sbfsFind
BNQ loadMissing
; A DIRECTORY IS REFUSED HERE AND NOT LEFT TO THE MAGIC CHECK BELOW. It has no blocks,
; so reading it reads nothing and leaves the staging area holding whatever was staged
; last - which, if that was a program, still says "SBEX" and still has a working entry
; address in it. Loading a directory would quietly hand back the program before it, and
; running it would look like the directory had run.
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ loadIsDirectory
SETD.1 0x80 0x00
CALL sbfsRead
BNQ loadUnreadable
; "SBEX", or this is not a program. Without this, loading a text file would put nonsense
; into Program Memory and then jump into the middle of it.
SETD.0 0x80 0x00
SETD.2 ExecMagic
INIA 0d4
SETD.1 LoadCount
STA.1
loadMagicLoop:
LDA.0
LDB.2
XOR
BNQ loadNotProgram
INCD.0
INCD.2
LDA.1
DECA
STA.1
BNA loadMagicLoop
; Version one is code and data. Version two also brings vectors, which is a thing a
; loader has to know how to do rather than a detail it can skip: a program whose handlers
; were quietly dropped would run and then go wrong somewhere with nothing to connect it
; back to here. Anything else is refused.
SETD.0 0x80 0x00
DPUP.0 0d04
LDA.0
SETD.1 LoadVersion
STA.1
INIB 0d1
XOR
BRQ loadVersionKnown
SETD.1 LoadVersion
LDA.1
INIB 0d2
XOR
BNQ loadWrongVersion
loadVersionKnown:
; The code. It comes from the staging area just past the sixteen byte header, and goes
; wherever the header says, in Program Memory, which the instruction set cannot write
; and the controller can.
INIA 0d1
OUTA 0xE0 ; SourceBank: Data Memory, where the file was staged.
INIA 0x80
OUTA 0xE1
INIA 0d16
OUTA 0xE2 ; 0x8010, the first byte after the header.
RSTA
OUTA 0xE3 ; DestBank: Program Memory.
SETD.0 0x80 0x00
DPUP.0 0d06
LDA.0
OUTA 0xE4
INCD.0
LDA.0
OUTA 0xE5
SETD.0 0x80 0x00
DPUP.0 0d10
LDA.0
OUTA 0xE6
INCD.0
LDB.0
OUTB 0xE7
; ---- Nothing to move is not the same as everything to move ----
;
; A LENGTH OF ZERO ASKS THE CONTROLLER FOR THE WHOLE 64K, which is the machine's rule and
; a sensible one for a length somebody typed: two bytes cannot say 65536, and a transfer
; of no bytes is not usually what anybody meant. It is exactly what is meant here. A
; segment can genuinely be empty - a five instruction program that only writes to a port
; has no data at all - and this asked to move 65536 bytes into a bank that has not got
; them, so the controller refused and the machine stopped in the middle of loading.
;
; The header says how long the segment is, so the loader knows before it asks. It has
; both bytes of the length in hand here, so testing them costs one instruction.
;
; NOTHING IS LOST BY SKIPPING IT. A blit leaves the controller's addresses past whatever
; it touched, and a blit of nothing would have left them exactly where they are.
OR
BRQ loadNoCode
INIA 0x01
OUTA 0xE8 ; Blit.
loadNoCode:
; Then the data. A blit leaves its addresses past whatever it touched, so the source is
; already sitting on the first byte of the data and only the destination changes.
INIA 0d1
OUTA 0xE3 ; DestBank: Data Memory.
SETD.0 0x80 0x00
DPUP.0 0d12
LDA.0
OUTA 0xE4
INCD.0
LDA.0
OUTA 0xE5
SETD.0 0x80 0x00
DPUP.0 0d14
LDA.0
OUTA 0xE6
INCD.0
LDB.0
OUTB 0xE7
; And the same for the data, which is the segment that is actually empty in practice.
OR
BRQ loadNoData
INIA 0x01
OUTA 0xE8 ; Blit.
loadNoData:
; ---- The vectors it brought ----
;
; Kept here rather than installed. A vector points into a program, so it has no business
; being in the table while that program is only loaded and not running: run puts them in
; and exit takes them out again, so the window they are live in is exactly the run.
; Keeping our own copy is also what lets a program be run more than once, since the
; staging area it came in on is fair game for the program's own use.
;
; Where to read them from is not worked out. The data blit left the controller's source
; address on the first byte after the data, which is where they are, so it is read back.
SETD.0 VectorSource
INA 0xE1
STA.0
INCD.0
INA 0xE2
STA.0
SETD.0 0x80 0x00
DPUP.0 0d05
LDA.0
SETD.1 LoadedVectorCount
STA.1
BRA loadVectorsCopied
; More than there is room for is refused rather than half taken. Half a program's
; handlers is not a smaller version of that program.
INIB 0d17
CCF
SUB
BNC loadTooManyVectors
SETD.0 VectorSource
LDD.2.0 ; DP2 walks the entries where they are staged.
SETD.3 LoadedVectors ; DP3 walks our own copy of them.
SETD.1 LoadedVectorCount
LDA.1
SETD.1 VectorsLeft
STA.1
loadVectorCopy:
; Four bytes: where it goes, then what goes there. The two bytes for what was there
; before are left alone until something is actually put in.
LDA.2
STA.3
INCD.2
INCD.3
LDA.2
STA.3
INCD.2
INCD.3
LDA.2
STA.3
INCD.2
INCD.3
LDA.2
STA.3
INCD.2
INCD.3
INCD.3
INCD.3
SETD.1 VectorsLeft
LDA.1
DECA
STA.1
BNA loadVectorCopy
loadVectorsCopied:
; Where it starts. Written out by hand rather than through a routine, because a routine
; could not hand two bytes back: CALL puts A, B and the first three pointers back the
; way it found them.
SETD.0 0x80 0x00
DPUP.0 0d08
LDA.0
SETD.1 LoadedEntry
STA.1
INCD.0
INCD.1
LDA.0
STA.1
INIA 0x01
SETD.0 LoadedOk
STA.0
RET ; LoadStatus is still the zero it was started at.
; Which of the seven happened, and where the words for it are. The two are set together so
; that no caller has to know both, and so that adding a way to fail cannot leave one of
; them behind.
loadNoDisk:
INIA 0d1
SETD.0 NoDisk
BRI loadRefuse
loadMissing:
INIA 0d2
SETD.0 NoSuchFile
BRI loadRefuse
loadUnreadable:
INIA 0d3
SETD.0 Unreadable
BRI loadRefuse
loadNotProgram:
INIA 0d4
SETD.0 NotProgram
BRI loadRefuse
loadWrongVersion:
INIA 0d5
SETD.0 WrongVersion
BRI loadRefuse
loadTooManyVectors:
INIA 0d6
SETD.0 TooManyVectors
BRI loadRefuse
loadIsDirectory:
INIA 0d7
SETD.0 IsDirectory
loadRefuse:
SETD.1 LoadMessage
STD.0.1
SETD.1 LoadStatus
STA.1
RET
; ---- Writing a file a block at a time ----
;
; Three handlers over the three routines in sbfs.asm, and thin, because everything that is
; difficult about writing safely is down there where it is written once.
;
; DP0 names the file, DP3 is how many whole blocks and A is what is left over in the last
; one - the size said the way an entry says it, which is what lets this reach past the
; 65,535 bytes osFileSave can describe.
handleFileStart:
SETD.2 DiskReady
PSHA
LDA.2
BRA fileStartNoDisk
POPA
; The tail is in A and the block count is in DP3, so the count has to come out of the
; pointer before anything else wants it.
SETD.2 SbfsFileTail
STA.2
; The low byte comes off the Stack first, which is the same way round Type and every
; other reader takes a count out of DP3. Popping them the other way gave a block count
; of the size times two hundred and fifty six, and a start that could find no room.
PSHD.3
POPB
POPA
SETD.2 SbfsFileBlocks
STA.2
INCD.2
STB.2
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsStreamStart
SRET
fileStartNoDisk:
POPA
INIA 0d1
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; DP1 is where the block comes from, and A and B together say which block of the file it
; is, counting from zero - the same way osFileBlock is told which one to fetch.
handleFileWrite:
SETD.2 SbfsIndex
STA.2
INCD.2
STB.2
CALL sbfsStreamWrite
SRET
; DP1 is where the block goes, and A and B together say which one, the same way writing is
; told. Reads back a block of the file being written.
handleFileFetch:
SETD.2 SbfsIndex
STA.2
INCD.2
STB.2
CALL sbfsStreamFetch
SRET
; DP3 is how many whole blocks it came to and A is what is left over, told the same way
; osFileStart is told. It need not be what was asked for: a writer that cannot know its
; size until the last byte asks for enough at the start, where running out costs nothing,
; and says the truth here. The blocks it did not use go back.
;
; This is the only step that can lose anything, and the last one.
handleFileDone:
SETD.2 SbfsFileTail
STA.2
PSHD.3
POPB
POPA
SETD.2 SbfsFileBlocks
STA.2
INCD.2
STB.2
CALL fileForget
CALL sbfsStreamDone
SRET
; ---- osChangeDir ----
;
; The service behind the shell's cd, and the reason the shell bothers to put the working
; directory back when a program stops: without something a program can call, that promise
; would have been about a thing that could not happen.
;
; DP0 names a directory. Q is zero if the machine is now in it.
handleChangeDir:
; DP0 arrives holding the path, because an interrupt frame keeps the caller's registers
; and this runs with them still in place. What it hands BACK has to be written into the
; frame, since RETI puts every register back the way the caller had it - which is why Q
; is stored at DP2 plus two below rather than simply being set.
SETD.2 DiskReady
LDA.2
BRA changeDirNo
CALL sbfsWalk
BNQ changeDirNo
; The root has no entry to ask about, and is always somewhere that can be stood in.
SETD.2 SbfsAt
LDA.2
INCD.2
LDB.2
OR
BRQ changeDirTake
SETD.2 SbfsFoundFlags
LDA.2
INIB 0x02
AND
BRQ changeDirNo
changeDirTake:
CALL fileForget ; A relative path means something else from here.
SETD.0 SbfsAt
SETD.1 SbfsCwd
CALL sbfsCopyWord
RSTA
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
changeDirNo:
INIA 0d1
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; ---- cd ----
;
; Moves the machine. What changes is two bytes, because the working directory is an entry
; index and nothing else: no path is stored anywhere, and the one on the prompt is worked
; out again each time from the chain of parents.
;
; "cd" on its own goes to the root, which is the only place that is always there and the
; only sensible thing to mean by home on a machine with no idea who is using it.
doCd:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA cdRoot
CALL sbfsWalk
BNQ cdNoSuch
; It has to be a directory to stand in. Ending at the root is fine and is the one case
; with no entry to ask, since the root is not an entry.
SETD.0 SbfsAt
LDA.0
INCD.0
LDB.0
OR
BRQ cdTake
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BRQ cdNotDirectory
cdTake:
; WHAT A RELATIVE PATH MEANS HAS JUST CHANGED, so the remembered file goes. It is keyed
; on the path as somebody typed it, and "notes.txt" is a different file from here than
; it was a moment ago. Nothing about the entry it remembers has changed, which is what
; makes this the kind of stale that is believed rather than noticed.
CALL fileForget
SETD.0 SbfsAt
SETD.1 SbfsCwd
CALL sbfsCopyWord
BRI prompt
cdRoot:
CALL fileForget
SETD.0 SbfsCwd
RSTA
STA.0
INCD.0
STA.0
BRI prompt
cdNoSuch:
SETD.0 NoSuchFile
BRI fileComplain
cdNotDirectory:
SETD.0 NotDirectory
BRI fileComplain
; ---- mkdir and rmdir ----
;
; Two commands rather than one that works out what you meant, and delete stays for files
; only. Each of the four says exactly what it will take, so none of them can be the one
; that took away more than was asked for.
doMkdir:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA mkdirWhat
CALL sbfsMakeDir
BNQ mkdirFailed
SETD.0 MadeText
CALL printString
CALL newLine
BRI prompt
mkdirWhat:
SETD.0 MkdirWhat
BRI fileComplain
mkdirFailed:
SETD.0 MkdirNo
BRI fileComplain
doRmdir:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA rmdirWhat
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsRemoveDir
BNQ rmdirFailed
SETD.0 RemovedText
CALL printString
CALL newLine
BRI prompt
rmdirWhat:
SETD.0 RmdirWhat
BRI fileComplain
rmdirFailed:
SETD.0 RmdirNo
BRI fileComplain
; ---- Writing out where the machine is ----
;
; Builds the working directory's path into CwdText and leaves CwdAt pointing at where it
; begins. Nothing stores a path, so this walks up the chain of parents.
;
; WRITTEN BACKWARDS, from the end of the buffer towards the front, because that is the
; order the names arrive in and reversing them afterwards would want somewhere to put them
; in the meantime. What comes back is a pointer into the middle of the buffer rather than
; to the front of it, which costs nothing to print from.
shellPath:
; The end of the buffer, holding the zero that ends the string.
SETD.0 CwdText
DPUP.0 0d126
RSTA
STA.0
SETD.1 CwdAt
STD.0.1
; HOW MUCH THERE IS TO WRITE INTO, COUNTED DOWN. Nothing bounds how deep the directories
; go: a path is capped at what one operation can name, but "mkdir a" and "cd a" are each
; well inside that and can be typed all day. This walk writes BACKWARDS from the end, so
; running out means walking off the FRONT of the buffer and into whatever the assembler
; put below it - which was the shell's own command names. Six directories of twenty two
; characters was enough, and the first five bytes to go were the word "exit", so the
; shell stopped recognising the command for leaving.
;
; A hundred and twenty three of the hundred and twenty six, the other three being kept
; back for the dots that say it was cut.
INIA 0d123
SETD.0 CwdRoom
STA.0
RSTA
SETD.0 CwdCut
STA.0
; Where the walk up starts.
SETD.0 SbfsCwd
SETD.1 CwdWalk
CALL sbfsCopyWord
shellPathStep:
SETD.0 CwdWalk
LDA.0
INCD.0
LDB.0
OR
BRQ shellPathDone ; The root, which is where every path begins.
SETD.0 CwdWalk
SETD.1 SbfsTarget
CALL sbfsCopyWord
SETD.0 SbfsTarget
CALL sbfsBackWord
CALL sbfsAtIndex
BNQ shellPathDone ; The disk would not read, so say as much as is known.
; The name goes in front of what is there, and a separator in front of that.
SETD.0 SbfsName
CALL shellPathPrepend
SETD.0 CwdCut
LDA.0
BNA shellPathReady ; It would not all fit, so there is nothing above worth asking for.
SETD.0 SbfsUpParent
SETD.1 CwdWalk
CALL sbfsCopyWord
BRI shellPathStep
shellPathDone:
; A machine at the root has written nothing at all, and the path to the root is the
; separator on its own.
SETD.0 CwdAt
LDD.1.0
LDA.1
BNA shellPathReady
SETD.0 Separator
CALL shellPathPrepend
shellPathReady:
RET
; DP0 names a string. Puts it in front of what CwdAt points at, with a separator before
; it, and moves CwdAt back over the lot.
;
; The string has to be measured before it can be written, since it is written from its
; last character backwards. Nothing here is long enough for that to be worth avoiding.
shellPathPrepend:
SETD.1 PathLength
RSTA
STA.1
pathMeasure:
LDA.0
BRA pathMeasured
SETD.1 PathLength
LDA.1
INCA
STA.1
INCD.0
BRI pathMeasure
pathMeasured:
; DP0 is on the zero at the end. Step back onto the last character, unless there is not
; one, in which case only the separator goes in.
SETD.1 CwdAt
LDD.2.1 ; DP2 is where the string already begins.
pathBack:
SETD.1 PathLength
LDA.1
BRA pathSeparator
DECA
STA.1
SETD.1 CwdRoom
LDA.1
BRA pathCut ; The front of the buffer, and the next byte would be past it.
DECA
STA.1
DECD.0
DECD.2
LDA.0
STA.2
BRI pathBack
pathSeparator:
SETD.1 CwdRoom
LDA.1
BRA pathCut
DECA
STA.1
DECD.2
INIA 0x2F
STA.2
SETD.1 CwdAt
STD.2.1
RET
; Stopping rather than writing past the front. The names go on backwards, so what is
; already down is the DEEP end of the path - which is the end worth showing: the prompt
; says where you are, and the last two directories say that better than the first two do.
;
; Three dots in front, out of the room that was never counted, so there is always
; somewhere to put them. No separator: the name they sit against brought its own, or was
; cut off half way, and "..." reads correctly against either.
pathCut:
INIA 0x2E
DECD.2
STA.2
DECD.2
STA.2
DECD.2
STA.2
SETD.1 CwdAt
STD.2.1
INIA 0x01
SETD.1 CwdCut
STA.1
RET
; ---- delete and rename ----
;
; The two things a disk needs that reading and writing do not provide, and the two that
; anything editing a document will want from the shell as well as from a program. Deleting
; frees an entry and its blocks; renaming changes twenty two bytes and moves nothing.
doDelete:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA deleteWhat
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsDelete
BNQ deleteFailed
SETD.0 Deleted
CALL printString
CALL newLine
BRI prompt
deleteWhat:
SETD.0 DeleteWhat
BRI fileComplain
deleteFailed:
; Two refusals arrive here as one. Asking again costs a walk of the directory, which is
; nothing on a path nobody takes twice, and it is the difference between "you typed a
; name that is not there" and "that is a directory, and delete does not take those".
SETD.1 TextRest
LDD.0.1
CALL sbfsFind
BNQ deleteNoSuch
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ deleteIsDirectory
deleteNoSuch:
SETD.0 NoSuchFile
BRI fileComplain
deleteIsDirectory:
SETD.0 IsDirectory
BRI fileComplain
doRename:
SETD.0 DiskReady
LDA.0
BRA fileNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA renameWhat
; Two names, so the rest of the line is split again. textSplit writes a zero over the
; space it cuts at, so what was one string becomes two without anything being copied.
SETD.1 TextRest
LDD.0.1
SETD.1 RenameFrom
STD.0.1
CALL textSplit
SETD.1 TextRest
LDD.1.1
LDA.1
BRA renameWhat ; Only one name was given, and this needs both.
SETD.2 RenameFrom
LDD.0.2
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsRename
BNQ renameFailed
SETD.0 Renamed
CALL printString
CALL newLine
BRI prompt
renameWhat:
SETD.0 RenameWhat
BRI fileComplain
renameFailed:
; Either there is no such file or the new name is already taken. Which of the two is not
; worth another message: both mean the disk does not have room for that name to move.
SETD.0 RenameNo
BRI fileComplain
fileNoDisk:
SETD.0 NoDisk
fileComplain:
CALL printString
CALL newLine
BRI commandFailed
; ---- drive ----
;
; Which disk the shell is standing on. With nothing after it, says which; with a number, goes
; there - and the working directory goes with it, because where you are on a disk is part of
; which disk you are on rather than something the shell keeps on the side.
doDrive:
SETD.1 TextRest
LDD.0.1
LDA.0
BRA driveSay
; One digit. Anything else is not a drive number, and the machines this is imitating never
; had ten drives either.
INIB 0d48
CCF
SUB
MVQA
INIB 0d10
CCF
SUB
BNC driveNoSuch ; Ten or more, so it was not a digit at all.
; Is there such a drive on this machine?
SETD.1 DriveWanted
STA.1
INB 0x25 ; How many drives, straight into B: there is no move from A to it.
LDA.1
CCF
SUB ; Borrows when the wanted one is inside the count.
BNC driveNoSuch
; And is there anything readable in it? A drive with no disk is a real drive and an empty
; one, so this is a different answer from "there is no such drive".
CALL sbfsDriveBit
MVQA
SETD.1 SbfsMounted
LDB.1
AND
BRQ driveNotReadable
SETD.1 DriveWanted
LDA.1
CALL sbfsUse
BRI prompt
driveSay:
INA 0x24
INIB 0d48
CCF
ADD
OUTQ 0x00
CALL newLine
BRI prompt
driveNoSuch:
SETD.0 DriveNoSuch
BRI fileComplain
driveNotReadable:
SETD.0 DriveNotReadable
BRI fileComplain
; ---- clear ----
;
; The console has done this since before there was a screen to do it on: writing 1 to the
; command port. On a terminal it is what a terminal does with it, and on the Voyager it is
; the screen going blank and the cursor going home.
doClear:
INIA 0x01
OUTA 0x05
BRI prompt
; ---- echo ----
;
; Says the rest of the line and nothing else. Say.sbx has done this since before there were
; scripts, and is the wrong shape for one: it is a program, so it has to be found on the
; disk and loaded and started, it prefixes what it was told with "it says:", and the system
; prints "finished" after it. That is three lines of noise around one line of narration.
;
; A script telling you what it is doing is the ordinary case now, so it costs a command
; rather than a program. With nothing after it, a blank line - which is what anybody will
; expect and is worth having for spacing a long script out.
doEcho:
SETD.1 TextRest
LDD.0.1
CALL printString
CALL newLine
BRI prompt
; ---- do ----
;
; Runs the lines in a file as though they had been typed. What makes a file one of these is
; the #! on the front of it, not its name and not a flag in its directory entry: the rule is
; that the entry holds only what the content cannot say about itself, and a script can say
; what it is. The loader already refuses anything that is not SBEX, so the two kinds of
; runnable thing turn each other away without either of them knowing about the other.
doScript:
SETD.1 TextRest
LDD.0.1
LDA.0
BRA scriptNoName
CALL scriptOpen
BRQ prompt ; It is open, and the next line read will come from it.
; Which of the two went wrong. A number would be no use to anybody here.
MVQA
INIB 0x01
CCF
SUB
BRQ scriptNoFile
INIB 0x02
CCF
SUB
BRQ scriptNotOne
SETD.0 ScriptTooDeep
BRI fileComplain
scriptNotOne:
SETD.0 ScriptNotOne
BRI fileComplain
scriptNoFile:
SETD.0 ScriptNoFile
BRI fileComplain
scriptNoName:
SETD.0 ScriptUsage
BRI fileComplain
; Where the person was standing when a program was started, so it can be given back when the
; program has finished with the machine. A program that copies between two disks moves the
; drive as its own paths need it to; that is its business, and being left on the disk it
; happened to finish with is not what the person asked for.
;
; SET WHEREVER A PROGRAM STARTS, of which there are two: run, and typing a program's name.
; Restored in handleExit, which is the one place they both come back through.
; ---- Putting the screen somewhere and getting it back ----
;
; A program that takes the whole screen leaves the shell a blank one, and everything that was
; on it - the listing you were reading, the error you were about to act on - is gone. There is
; nowhere to put 32K on a machine with 64K of Data Memory that CosmOS is already living in.
;
; A DRIVE MADE OF MEMORY IS SOMEWHERE. The map goes to a file on the scratch drive like any
; other file, and comes back the same way; the filesystem does the allocating, and this had to
; invent nothing at all to have somewhere to put it.
;
; EVERYTHING A PROGRAM CAN DISTURB, which is more than the part on screen. The map's off
; screen rows are the console's scrollback; the tiles are the font, which a program that
; redefines one has overwritten; and the palette is where the console's own colours live -
; Grid could give back the map and not the colours, and handed the shell green text on blue.
;
; 192 pages of tiles and map, which are next to each other, then the four of palette. 196 in
; all, and a register block on the front.
;
; ---- Saved on being asked, restored on the way out ----
;
; Saving on every program start would be cheap enough. Restoring on every exit would be
; WRONG: dir, Files and Say print and stop, and their output is the reason you ran them.
; So a program says it is taking the screen, and one that says nothing behaves exactly as
; every program did before this existed.
screenTake:
SETD.0 SbfsScratch1
LDA.0
INIB 0xFF
CCF
SUB
BRQ screenNoScratch ; No volatile drive, so nowhere to put it.
; Where the caller was, and where the file is going.
INA 0x24
SETD.0 ScreenWasDrive
STA.0
SETD.0 SbfsScratch1
LDA.0
CALL sbfsUse
CALL screenBank
; A hundred and twenty nine blocks: one of registers, then the map.
SETD.0 SbfsFileBlocks
RSTA
STA.0
INCD.0
INIA 0d197
STA.0
SETD.0 SbfsFileTail
RSTA
STA.0
SETD.0 ScreenFileName
CALL sbfsStreamStart
BNQ screenTakeFailed
; Block nought is where the screen was, rather than what was on it: the cursor, the four
; scroll registers and the mode. A picture put back under a different origin is not the
; picture that was taken.
CALL screenClearBlock
SETD.0 ScreenBlock
INA 0x03
STA.0
INCD.0
INA 0x04
STA.0
INCD.0
INA 0x34
STA.0
INCD.0
INA 0x36
STA.0
INCD.0
INA 0x37
STA.0
INCD.0
INA 0x38
STA.0
INCD.0
INA 0x31
STA.0
RSTA
RSTB
SETD.2 SbfsIndex
STA.2
INCD.2
STB.2
SETD.1 ScreenBlock
CALL sbfsStreamWrite
BNQ screenTakeFailed
; And the map, a block at a time through the one buffer there is.
RSTA
SETD.0 ScreenAt
STA.0
screenTakeBlock:
SETD.0 ScreenAt
LDA.0
CALL screenPageFor
MVQA
CALL screenFromVideo
SETD.0 ScreenAt
LDA.0
INCA
SETD.2 SbfsIndex
RSTB
STB.2
INCD.2
STA.2 ; Block n of the map is block n+1 of the file.
SETD.1 ScreenBlock
CALL sbfsStreamWrite
BNQ screenTakeFailed
SETD.0 ScreenAt
LDA.0
INCA
STA.0
INIB 0d196
CCF
SUB
BNQ screenTakeBlock
SETD.0 SbfsFileBlocks
RSTA
STA.0
INCD.0
INIA 0d197
STA.0
SETD.0 SbfsFileTail
RSTA
STA.0
CALL sbfsStreamDone
BNQ screenTakeFailed
INIA 0x01
SETD.0 ScreenSaved
STA.0
CALL screenGoBack
RSTA
RSTB
CCF
ADD
RET
screenTakeFailed:
RSTA
SETD.0 ScreenSaved
STA.0
CALL screenGoBack
screenNoScratch:
RSTA
INIB 0d1
CCF
ADD
RET
; Whatever screenTake put away, put back. Nothing at all if it never ran.
screenGive:
SETD.0 ScreenSaved
LDA.0
BRA screenGiveNone
RSTA
STA.0 ; Once only: the next program takes its own.
INA 0x24
SETD.0 ScreenWasDrive
STA.0
SETD.0 SbfsScratch1
LDA.0
CALL sbfsUse
CALL screenBank
SETD.0 ScreenFileName
CALL fileLookup
BNQ screenGiveDone
; The map first, then the registers, so that nothing is drawn under an origin that is about
; to change.
RSTA
SETD.0 ScreenAt
STA.0
screenGiveBlock:
SETD.0 ScreenAt
LDA.0
INCA
SETD.2 SbfsIndex
RSTB
STB.2
INCD.2
STA.2
SETD.1 ScreenBlock
CALL sbfsReadOne
BNQ screenGiveDone
SETD.0 ScreenAt
LDA.0
CALL screenPageFor
MVQA
CALL screenToVideo
SETD.0 ScreenAt
LDA.0
INCA
STA.0
INIB 0d196
CCF
SUB
BNQ screenGiveBlock
; And where it was.
RSTA
RSTB
SETD.2 SbfsIndex
STA.2
INCD.2
STB.2
SETD.1 ScreenBlock
CALL sbfsReadOne
BNQ screenGiveDone
SETD.0 ScreenBlock
LDA.0
OUTA 0x03
INCD.0
LDA.0
OUTA 0x04
INCD.0
LDA.0
OUTA 0x34
INCD.0
LDA.0
OUTA 0x36
INCD.0
LDA.0
OUTA 0x37
INCD.0
LDA.0
OUTA 0x38
INCD.0
LDA.0
OUTA 0x31
screenGiveDone:
CALL screenGoBack
screenGiveNone:
RET
; The registers a console cannot work without, put back. Not the picture - that is
; screenGive's, and only happens for a program that asked.
screenSane:
RSTA
OUTA 0x37
OUTA 0x38 ; No fraction of a cell in either direction.
OUTA 0x3C ; And the screen everybody else's map is in.
; ---- 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
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
CALL installVectors
; The entry address is a number until BRD makes it a place. DP3 is the one to build it
; in, because it is the pointer nothing puts back.
SETD.1 LoadedEntry
LDD.3.1
BRD.3
runNothing:
SETD.0 NothingLoaded
CALL printString
CALL newLine
BRI commandFailed
; DP0 is a string, DP1 is where it should go, and B is how much room there is counting
; the zero on the end. What does not fit is left behind, and what is written is a string
; either way.
copyText:
BRB copyTextDone ; No room at all, so nothing is written, not even the zero.
copyTextLoop:
DECB
BRB copyTextEnd ; Only room for the terminator now.
LDA.0
STA.1
BRA copyTextDone
INCD.0
INCD.1
BRI copyTextLoop
copyTextEnd:
RSTA
STA.1
copyTextDone:
RET
; ---- Putting a program's vectors in, and taking them out again ----
;
; The vector table lives in Program Memory, which no instruction can write, so both of
; these go through the memory controller. Port 0xE9 reads a byte from the source and writes
; a byte to the destination, stepping the address on either way, so a two byte entry is two
; reads or two writes and no address arithmetic in between.
;
; What was in the slot is kept before anything replaces it, and put back afterwards, rather
; than the slot being cleared. Clearing would be wrong wherever a program has installed a
; handler over one the system was already using: the program is allowed to do that, and
; when it goes, what it covered up has to come back rather than becoming a hole.
installVectors:
SETD.0 LoadedVectorCount
LDA.0
BRA installDone
SETD.1 VectorsLeft
STA.1
SETD.3 LoadedVectors
installOne:
; DP3 walks one six byte entry: where it goes, what goes there, and room for what was
; there before. Reading and writing the same slot, so the controller is pointed at it
; from both ends at once and the address is only worked out once.
RSTA
OUTA 0xE0 ; SourceBank: Program Memory.
OUTA 0xE3 ; DestBank: the same.
LDA.3
OUTA 0xE1
OUTA 0xE4
INCD.3
LDA.3
OUTA 0xE2
OUTA 0xE5
INCD.3 ; On the handler.
; What is there now, before anything replaces it.
INA 0xE9
PSHA
INA 0xE9
PSHA
; And the handler in its place.
LDA.3
OUTA 0xE9
INCD.3
LDA.3
OUTA 0xE9
INCD.3 ; On the two bytes kept for what was there before.
; The Stack gives them back in the reverse of the order they went on, so the low byte
; arrives first and is written to the second of the two. Getting this the natural way
; round instead put the low byte where the high one goes and the high byte over the
; handler, which the first run of a program survives - the table is already written by
; then - and the second run does not.
POPA
INCD.3
STA.3
DECD.3
POPA
STA.3
INCD.3
INCD.3
SETD.1 VectorsLeft
LDA.1
DECA
STA.1
BNA installOne
installDone:
RET
removeVectors:
SETD.0 LoadedVectorCount
LDA.0
BRA removeDone
SETD.1 VectorsLeft
STA.1
SETD.3 LoadedVectors
removeOne:
RSTA
OUTA 0xE3 ; DestBank: Program Memory.
LDA.3
OUTA 0xE4
INCD.3
LDA.3
OUTA 0xE5
INCD.3
INCD.3
INCD.3 ; Past the handler, to what was underneath it.
LDA.3
OUTA 0xE9
INCD.3
LDA.3
OUTA 0xE9
INCD.3
SETD.1 VectorsLeft
LDA.1
DECA
STA.1
BNA removeOne
removeDone:
RET
; ---- The services ----
;
; These are what a loaded program is allowed to ask for. The names and their numbers come
; from services.asm, which the programs include as well, so neither side writes a number
; down and the two cannot disagree about them.
;
; A handler arrives with the caller's registers exactly as they were: an interrupt frame
; is pushed, not cleared. So the pointer a program put in DP0 is still there to be used.
; The disk finishing, acknowledged and ignored.
;
; The system drives the disk by asking its status port and waiting, so it has no use for
; the line. But the disk raises one after every operation whether anybody wants it or not,
; and a line goes on waiting while the Interrupt Flag is down rather than being lost. The
; shell keeps the flag down, so the line from the last disk read was still standing when
; the first program to enable interrupts ran, and it arrived there - a fault, in a program
; that had never heard of the disk, blamed on the innocent instruction that let it through.
;
; Answering a line is what takes it down, so this is one instruction and that is the point.
diskDone:
RETI
handlePrintString:
CALL printString
RETI
handleReadLine:
CALL editLinePlain
; readLine works out how long the line was, and it is already in Q where readLine left
; it. SRET keeps Q and DP3 and puts everything else back, so the answer simply stands.
SRET
; What the program was asked to work on. DP0 says where to put it and B how much room
; there is, counting the zero on the end, which is the same bargain readLine offers.
;
; Being asked for rather than left at an agreed address is deliberate. The two sides of
; this already have to agree on a vector number and nothing else, and that number is
; written down once in services.asm; an address would be a second thing to agree about, in
; a memory map that is a convention rather than anything enforced.
handleArgument:
PSHD.0
POPD.1
SETD.0 RunArgument
CALL copyText
RETI
; ---- The disk, on a program's behalf ----
;
; A loaded program that wanted a file used to include the whole filesystem, so it carried a
; private copy of code the system already has running, and mounted a disk that was already
; mounted. These are that code, reachable through a number instead.
;
; Every one of them answers in Q, and the answer is written into the frame over the saved
; register, because RETI puts every register back and would otherwise throw it away. That
; has to be done here rather than in a routine of its own: the offsets are from where the
; Stack Pointer is, and a CALL moves it by ten.
;
; A machine with no disk answers no to all of them rather than going ahead and finding out,
; because sbfs on a disk that was never mounted is reading whatever bank 3 happens to be.
; DP0 names the file, DP1 says where to put it. Q is zero if it read, and DP3 comes back
; holding how many bytes there were.
handleFileRead:
SETD.2 DiskReady
LDA.2
BRA fileReadNo
CALL sbfsFind
BNQ fileReadNo
; A file of 256 blocks is 64K, which will not fit in Data Memory and will not fit in the
; pointer that says how long it is either. Refused, rather than read as much of as fits:
; a length that lies is worse than a file that will not open.
SETD.2 SbfsFileBlocks
LDA.2
BNA fileReadNo
CALL sbfsRead
BNQ fileReadNo
; How long it is: the block count is the high byte of that and the tail is the low one,
; which is how a size is put together everywhere on this disk.
SETD.2 SbfsFileBlocks
INCD.2
LDA.2
SETD.2 SbfsFileTail
LDB.2
; A two byte answer, and DP3 is where a routine hands one back. Built with the
; Stack rather than written into the frame: SRET leaves DP3 alone, so there is
; nothing to reach into. PSHA then PSHB puts the high byte above the low one,
; which is the order POPD reads them in.
PSHA
PSHB
POPD.3
RSTA
RSTB
CCF
ADD ; Q is zero: it is there.
SRET
fileReadNo:
INIA 0d1
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; DP0 names the file, DP1 is the bytes, and A and B together are how many. Q is zero if it
; saved. Whether it was there before makes no difference, which is what saving means.
handleFileSave:
SETD.2 DiskReady
PSHA
LDA.2
BRA fileSaveNoDisk
POPA
; Blocks are the high half of the count and the tail is the low half.
SETD.2 SbfsFileBlocks
PSHA
RSTA
STA.2 ; A whole file's block count fits in a byte, so this is zero.
INCD.2
POPA
STA.2
SETD.2 SbfsFileTail
STB.2
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsSaveFile
SRET
fileSaveNoDisk:
POPA
INIA 0d1
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; DP0 names it. Q is zero if it went.
handleFileDelete:
SETD.2 DiskReady
LDA.2
BRA serviceNoDisk
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsDelete
SRET
; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved.
handleFileRename:
SETD.2 DiskReady
LDA.2
BRA serviceNoDisk
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsRename
SRET
serviceNoDisk:
INIA 0d1
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; ---- Reading a file that will not fit ----
;
; A file bigger than Data Memory cannot be handed over whole, and CosmOS's own source is
; now that file, so these two are how anything reads one: ask how many blocks, then ask for
; each block in turn. There is no open and no close. A program that stops halfway through
; leaves nothing behind, because there was never anything to leave.
; Finds a file, or remembers that it already did. DP0 names it. Q is zero if it is there,
; and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it exactly the way
; sbfsFind leaves them - whether the search happened or not, which is the whole point.
fileLookup:
SETD.2 FileCacheValid
LDA.2
BRA fileLookupSearch
SETD.1 FileCacheName
CALL textSame
BNQ fileLookupSearch
; ---- The same file as last time, and possibly not the same disk ----
;
; The description has to be put back, because anything that went to the disk in between - a
; directory listing, a program being loaded - left its own answer in those three.
;
; AND THE DRIVE WITH IT. Skipping the walk skips the drive the path named, so a cache hit
; on a machine that had moved read the right block numbers off the wrong disk. Copying
; between two disks is exactly that: block 0 walks and goes to the source, the write goes to
; the destination, and block 1 hits this cache. It only showed on files of more than one
; block, because a file of one is never looked up twice.
SETD.0 FileCacheDrive
LDA.0
CALL sbfsUse
SETD.0 SbfsFileStart
SETD.2 FileCacheStart
CALL sbfsSetWord
SETD.0 SbfsFileBlocks
SETD.2 FileCacheBlocks
CALL sbfsSetWord
SETD.0 FileCacheTail
LDA.0
SETD.0 SbfsFileTail
STA.0
RSTA
RSTB
CCF
ADD ; Q is zero: found.
RET
fileLookupSearch:
CALL sbfsFind
BNQ fileLookupMissing
; Remember it. DP0 still names the file: a CALL puts the pointers back, which is the one
; place that convention is a convenience rather than an obstacle.
;
; A WHOLE PATH IS KEPT, not the twenty two bytes a name has. Keeping twenty two of a
; longer path cannot hand back the wrong file - textSame wants both strings to end in
; the same place, so a cut down entry misses rather than matching something else - but
; it can never match either, so every path longer than a name would go to the disk every
; single time and the cache would quietly stop being one.
SETD.1 FileCacheName
INIB 0d64
CALL copyText
SETD.0 FileCacheStart
SETD.2 SbfsFileStart
CALL sbfsSetWord
SETD.0 FileCacheBlocks
SETD.2 SbfsFileBlocks
CALL sbfsSetWord
SETD.0 SbfsFileTail
LDA.0
SETD.0 FileCacheTail
STA.0
; ---- And which disk those block numbers are on ----
;
; A start block means nothing without it. The walk above went to whatever drive the path
; named, so the answer is the drive now.
INA 0x24
SETD.0 FileCacheDrive
STA.0
; Marked good last, so that a cache half filled is never a cache believed.
INIA 0d1
SETD.0 FileCacheValid
STA.0
RSTA
RSTB
CCF
ADD ; Q is zero: found.
RET
fileLookupMissing:
RET ; Q is not zero, and sbfsFind is what made it so.
; Throws the remembered file away. Everything that can change what a name means on the disk
; calls this before it does: a file saved over may have moved, a deleted one is gone, and a
; renamed one answers to something else. A remembered start block that survived any of
; those is a pointer at whatever took its place.
;
; Q is deliberately untouched, so this can be dropped into a handler without disturbing the
; answer that handler is in the middle of working out.
fileForget:
RSTA
SETD.0 FileCacheValid
STA.0
RET
; DP0 names it. Q is zero if it is there, and DP3 comes back holding how many blocks it
; occupies, counting a part one on the end.
;
; Blocks, not bytes, and that is forced rather than chosen: a file on a sixteen megabyte
; disk is up to twenty four bits long, which does not fit in a pointer. Blocks do, and the
; bytes in the last one come back from osFileBlock when the reader gets there.
handleFileInfo:
SETD.2 DiskReady
LDA.2
BRA fileInfoNoDisk
CALL fileLookup
BNQ fileInfoMissing
CALL sbfsFileExtent
SETD.2 SbfsWantBlocks
LDA.2
INCD.2
LDB.2
; A two byte answer, and DP3 is where a routine hands one back. Built with the
; Stack rather than written into the frame: SRET leaves DP3 alone, so there is
; nothing to reach into. PSHA then PSHB puts the high byte above the low one,
; which is the order POPD reads them in.
PSHA
PSHB
POPD.3
RSTA
RSTB
CCF
ADD ; Q is zero: it is there.
SRET
fileInfoNoDisk:
INIA 0d1
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
fileInfoMissing:
INIA 0d2
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; DP0 names it, DP1 says where to put it, and A and B together are which block, counting
; from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes
; belong to the file.
handleFileBlock:
; Which block, before anything else, because finding out whether there is a disk needs A
; and there is nowhere else the number is written down.
SETD.2 SbfsIndex
STA.2
INCD.2
STB.2
SETD.2 DiskReady
LDA.2
BRA fileBlockNoDisk
CALL fileLookup
BNQ fileBlockMissing
; Running off the end is how a reader finds out it has finished, so it gets an answer of
; its own rather than being told the disk failed.
CALL sbfsFileExtent
SETD.0 SbfsIndex
SETD.2 SbfsWantBlocks
CALL sbfsCompareWord
BNC fileBlockPastEnd ; The Carry is set only when the index is the smaller.
CALL sbfsReadOne ; DP1 still says where. A CALL puts the pointers back.
BNQ fileBlockFailed
; How much of it is the file's. Every block but a short last one is a whole 256, and 256
; is why this answers in a pointer instead of a register.
SETD.2 SbfsFileTail
LDA.2
BRA fileBlockWhole ; Nothing partial on the end, so they are all whole.
SETD.0 SbfsIndex
SETD.2 SbfsFileBlocks
CALL sbfsCompareWord
BNQ fileBlockWhole ; Not the last one.
SETD.2 SbfsFileTail
LDB.2
RSTA
BRI fileBlockAnswer
fileBlockWhole:
INIA 0x01
RSTB
fileBlockAnswer:
; A two byte answer, and DP3 is where a routine hands one back. Built with the
; Stack rather than written into the frame: SRET leaves DP3 alone, so there is
; nothing to reach into. PSHA then PSHB puts the high byte above the low one,
; which is the order POPD reads them in.
PSHA
PSHB
POPD.3
RSTA
RSTB
CCF
ADD ; Q is zero: it is there.
SRET
fileBlockNoDisk:
INIA 0d1
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
fileBlockMissing:
INIA 0d2
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
fileBlockPastEnd:
INIA 0d3
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
fileBlockFailed:
INIA 0d4
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; A breakpoint. Shows every register as the interrupted program had them, waits for a key,
; and returns as though nothing happened.
;
; EVERY VALUE COMES OUT OF THE FRAME, not out of the registers, because by the time this
; runs the registers belong to the handler. The frame is what the program had, and RETI is
; going to give it all back, so what is shown is what will be resumed with.
;
; DP3 holds the frame throughout. It survives a CALL, and console.asm promises not to
; disturb it, which is what lets the printing routines be used between one field and the
; next. The Stack Pointer comes back to the same place after a balanced call, so the frame
; stays where it was found.
;
; +1 Status +2 Q +3 A +4 B +5 DP3 +7 DP2 +9 DP1 +11 DP0 +13 where it resumes
handleBreak:
MVSD.3
; Where it broke, which is two before where it resumes: the SWI and the vector it names.
SETD.0 BreakText
CALL printString
PSHD.3
POPD.0
DPUP.0 0d13
LDA.0
PSHA ; The high half, while the low one is worked on.
INCD.0
LDA.0
INIB 0d2
CCF
SUB ; Two back from where it resumes: the SWI and the vector it names.
MVQA
POPB
BRC breakBorrowed ; It borrowed, so the high half comes down by one.
BRI breakAddress
breakBorrowed:
DECB
breakAddress:
PSHA ; low
PSHB ; high
POPA
CALL printByteHex
POPA
CALL printByteHex
CALL newLine
SETD.0 ARegText
PSHD.3
POPD.1
DPUP.1 0d3
CALL breakByte
SETD.0 BRegText
PSHD.3
POPD.1
DPUP.1 0d4
CALL breakByte
SETD.0 QRegText
PSHD.3
POPD.1
DPUP.1 0d2
CALL breakByte
SETD.0 SRegText
PSHD.3
POPD.1
INCD.1
CALL breakByte
; And which bits of it those are, since a debug dump that makes you look the number up is
; only half of one. Halt is not among them: the machine is plainly not halted.
PSHD.3
POPD.1
INCD.1
LDA.1
PSHA
INIB 0x01
AND
BRQ breakNoCarry
SETD.0 CarryText
CALL printString
breakNoCarry:
POPA
PSHA
INIB 0x02
AND
BRQ breakNoFault
SETD.0 FaultText
CALL printString
breakNoFault:
POPA
INIB 0x04
AND
BRQ breakNoInts
SETD.0 IntsText
CALL printString
breakNoInts:
CALL newLine
SETD.0 DP0Text
PSHD.3
POPD.1
DPUP.1 0d11
CALL breakWord
SETD.0 DP1Text
PSHD.3
POPD.1
DPUP.1 0d9
CALL breakWord
SETD.0 DP2Text
PSHD.3
POPD.1
DPUP.1 0d7
CALL breakWord
SETD.0 DP3Text
PSHD.3
POPD.1
DPUP.1 0d5
CALL breakWord
; The Stack Pointer is not in the frame, because the frame is where the Stack Pointer is.
; What the program had is fourteen bytes above this one, that being what entering an
; interrupt puts down.
SETD.0 SPText
CALL printString
PSHD.3
POPD.0
DPUP.0 0d14
PSHD.0
POPB
POPA
CALL printByteHex
PSHB
POPA
CALL printByteHex
CALL newLine
; Anything typed carries on. Reading the data port waits however the console is set, which
; is one key if the program asked for key mode and a whole line if it did not - and either
; way it is the program's own console being borrowed for a moment.
SETD.0 ResumeText
CALL printString
INA 0x00
CALL newLine
RETI
; DP0 names a field and DP1 points at it in the frame. The caller does the stepping, with
; DPUP and a number written into the program, because a routine cannot hand a pointer back:
; CALL saves DP0 to DP2 and RET puts them back, so a walk done in here would be undone on
; the way out. Written that way first, and every field showed the frame's first byte.
breakByte:
CALL printString
LDA.1
CALL printByteHex
INIA 0x20
OUTA 0x00
RET
; The same for the two byte fields, most significant first the way the frame holds them.
breakWord:
CALL printString
LDA.1
CALL printByteHex
INCD.1
LDA.1
CALL printByteHex
INIA 0x20
OUTA 0x00
RET
; A and B together are a number. Prints it in decimal without leading zeroes, which covers
; a line number and a byte count both, so there is no need for one service each.
handlePrintNumber:
SETD.0 PrintNumber
STA.0
INCD.0
STB.0
SETD.0 PrintNumber
CALL printWordDecimal
RETI
; Giving the machine back. This is the one place MVDS earns its keep. The program's Stack,
; and the frame this very interrupt arrived on, are both abandoned where they lie, because
; nothing is going to return through either of them.
;
; Which is exactly why this cannot RETI. Its return address is on the Stack it just walked
; away from, so it branches to the prompt instead.
; What the last program made of what it was asked to do. Kept rather than shown: a program
; that failed has already said so in words, and a number beside that would be noise. It is
; here for the thing that cannot read words - whatever comes to run programs in sequence and
; has to decide whether to run the next one.
;
; IT IS NOT LineFailed, and the difference is worth keeping. This one is a PROGRAM'S answer
; and belongs to whoever asks for it, which the shell's own status command does and a test
; records. Zeroing it as each line began - which is what the script reader wanted - wiped
; the answer before the command that reports it could read its own line. Two questions, two
; bytes.
handleLastStatus:
SETD.2 LastStatus
LDA.2
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; ---- How the last start went, and settling it ----
;
; The answer goes in Q by writing into this handler's own frame, which is how every service
; here hands anything back: a handler arrives with the caller's registers pushed, not
; cleared, and RETI puts them back - so the way to return a value is to change the copy the
; return is going to restore.
handleBootState:
SETD.2 DiskReady
LDA.2
BRA bootStateNone
CALL sbfsBootState
BNQ bootStateNone
SETD.2 SbfsStateWas
LDA.2
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
bootStateNone:
; No disk, or one that would not answer. Nothing there to be unsettled about.
RSTA
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
handleBootSettle:
SETD.2 DiskReady
LDA.2
BRA bootSettleNo
RSTA
CALL sbfsSetBootState
BNQ bootSettleNo
RSTA
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
bootSettleNo:
INIA 0d1
RSTB
CCF
ADD ; A is the answer, so Q becomes it.
SRET
; The program is about to draw over everything. Q says whether what is there now will come
; back, and a program that is told no carries on anyway.
handleTakeScreen:
CALL screenTake
MVQA
MVSD.2
DPUP.2 0d02
STA.2
RETI
handleExit:
; ---- What the program made of it ----
;
; A, before anything below disturbs it. IN A RATHER THAN Q, and that is not a departure
; from the rule that a service answers in Q: this one takes an argument, the way
; osPrintNumber takes A and B, and it never returns to answer anything. The register a
; subroutine cannot hand anything back in is exactly the one that is free here - and Q is
; the ALU's output, so setting it to a small number costs four instructions where A costs
; one.
SETD.1 LastStatus
STA.1
; ---- The screen made usable, whether or not the picture can be given back ----
;
; A fraction of a cell is never what anybody wants left behind: the console draws in whole
; cells, so a view three pixels into one puts every character three pixels out for ever.
; That is true whether or not there was anywhere to save the picture, so it is not part of
; the saving - a program refused by osTakeScreen still must not leave the shell squinting.
;
; Before screenGive, so that a restored screen's own registers win.
CALL screenSane
; What was on the screen before this program had it, if it asked for that.
CALL screenGive
; The drive the person was on, whatever the program did with it.
PSHA
SETD.1 RunDrive
LDA.1
CALL sbfsUse
POPA
; And the line that started it failed, if the program says it did. The script reader asks
; one question - did this line work - and a program answering "no" is one of the ways it
; can be answered.
BRA exitWorked
INIA 0x01
SETD.1 LineFailed
STA.1
exitWorked:
SETD.1 SystemStack
LDD.0.1
MVDS.0
; Whatever the program put in the vector table comes out again. A vector points into the
; program that supplied it, and the program is gone, so anything left installed would aim
; an interrupt at whatever those addresses hold next.
CALL removeVectors
; And where the machine was before the program had it, for the same reason and by the
; same discipline as the Stack above and the vectors just now: a program is entitled to
; move about, and the shell is entitled to find itself where it left off. What a relative
; path means has changed back, so the remembered file goes with it.
SETD.0 SavedCwd
SETD.1 SbfsCwd
CALL sbfsCopyWord
CALL fileForget
; The console goes back to how the shell wants it, whatever the program left it in: line
; mode, and not interrupting. A program that wanted either is expected to put it back
; itself, but one that stopped early, or forgot, would otherwise hand back a shell with
; no echo and no backspace, or one being interrupted about keys it is reading anyway.
; This undoes everything the control port can be asked for and puts the cursor back, and
; asking for what is already the case costs a byte out of a port and does nothing. That
; is the right price for not having to know.
;
; THE CURSOR IS PUT BACK RATHER THAN LEFT, because a program that borrowed key mode and
; handed it back the way it was told to writes zero, which turns the cursor off. The shell
; owns the prompt, so the shell is what makes sure there is something blinking at it.
INIA 0x04
OUTA 0x02
; The ink too, and for the same reason as the cursor: a program that chose a colour is not
; around to put it back, and neither is one the fault screen printed for. The shell owns
; the prompt, so the shell is what makes sure it is readable.
RSTA
OUTA 0x06
; ---- Finished, or stopped ----
;
; A program that faulted did not finish, and saying so would be the shell's own word
; against what the fault screen just said in red immediately above it.
SETD.1 FaultStopped
LDA.1
BRA exitFinished
RSTA
STA.1 ; Cleared, so the next program is not blamed for this one.
SETD.0 Stopped
BRI exitSay
exitFinished:
SETD.0 Finished
exitSay:
CALL printString
CALL newLine
BRI prompt
; ---- dump ----
;
; dump Sixty four more bytes, carrying on from the last one.
; dump <where> From the start of that bank.
; dump <where> <addr> From there.
;
; <where> is program, data, or a bank number in hexadecimal. That the CPU cannot read
; Program Memory and this can is the whole point: the instruction set has no way to look
; at itself, and the controller does, so a monitor is possible at all only through it.
; b <program|data|number>
;
; 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 <address> <byte> <byte> ...
;
; 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 <address>
;
; 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 <address>, then instructions until a line that is just a dot. The syntax is the
; assembler's - a selector rides on the mnemonic as LDA.0 or LDD.0.1, and leaving one off
; means Data Pointer 0, exactly as it does in a source file - so nothing learned here has to
; be unlearned when writing a real program.
;
; NUMBERS ARE HEXADECIMAL AND BARE. A source file writes 0x2000 or 0d16 because it has both
; and must say which; a monitor has only one and says so once, in the manual, rather than on
; every line. It is the same reason x and d take bare addresses.
;
; What cannot be written here is a label, and that is the whole difference between this and
; the assembler proper: a label is a promise to fill an address in later, and later is what
; a line at a time does not have.
doAssemble:
SETD.1 TextRest
LDD.0.1
CALL textHexWord
BNQ assembleWhat
SETD.0 TextValue
SETD.1 DumpAt
CALL sbfsCopyWord ; Two bytes from DP0 to DP1, which sbfs already has.
assembleLine:
SETD.0 DumpAt
CALL printWordHex
SETD.0 AsmPrompt
CALL printString
SETD.0 AsmLine
INIB 0d40
CALL shellReadRaw
INA 0x01
INIB 0x02 ; ENDED, so there is nothing more to assemble.
AND
BNQ prompt
SETD.0 AsmLine
LDA.0
BRA assembleLine ; An empty line is somebody thinking.
SETD.0 AsmLine
SETD.1 DotText
CALL textSame
BRQ prompt
SETD.0 AsmLine
CALL textSplit ; The mnemonic, and whatever follows it.
CALL assembleOne
BRI assembleLine
assembleWhat:
SETD.0 AsmUsage
CALL printString
CALL newLine
BRI commandFailed
; The mnemonic is in CommandLine's place - AsmLine - and TextRest is what followed it.
; Puts the bytes down and steps the cursor past them.
assembleOne:
CALL takeMnemonic
CALL findByName
BNQ assembleUnknown
; WHATEVER IT NEEDS IS READ BEFORE ANYTHING IS WRITTEN. Emitting the opcode first and
; discovering the missing value afterwards leaves half an instruction in memory, which the
; next line usually covers up and the last line of a session does not.
SETD.0 AsmShape
LDA.0
BRA assemblePut ; 0, nothing to read.
DECA
BRA assembleWantAddress ; 1
DECA
BRA assembleWantByte ; 2
DECA
BRA assemblePut ; 3, a selector and nothing else.
DECA
BRA assembleWantByte ; 4
DECA
BRA assembleWantAddress ; 5
BRI assemblePut ; 6, two selectors.
assembleWantByte:
SETD.1 TextRest
LDD.0.1
CALL textHexWord
BNQ assembleNeedsValue
BRI assemblePut
assembleWantAddress:
SETD.1 TextRest
LDD.0.1
CALL textHexWord
BNQ assembleNeedsValue
assemblePut:
; Point the controller at the cursor. Every byte written steps it on by itself.
SETD.0 DumpBank
LDA.0
OUTA 0xE3
SETD.0 DumpAt
LDA.0
OUTA 0xE4
INCD.0
LDA.0
OUTA 0xE5
SETD.0 AsmOpcode
LDA.0
OUTA 0xE9
; What follows depends only on the shape, exactly as it does when reading one back.
SETD.0 AsmShape
LDA.0
BRA assembleDone ; 0
DECA
BRA assembleAddress ; 1
DECA
BRA assembleByte ; 2
DECA
BRA assembleSelector ; 3
DECA
BRA assembleSelByte ; 4
DECA
BRA assembleSelAddress ; 5
SETD.0 AsmSelOne
LDA.0
OUTA 0xE9
SETD.0 AsmSelTwo
LDA.0
OUTA 0xE9
BRI assembleDone
assembleSelector:
SETD.0 AsmSelOne
LDA.0
OUTA 0xE9
BRI assembleDone
assembleSelByte:
SETD.0 AsmSelOne
LDA.0
OUTA 0xE9
BRI assembleByte
assembleSelAddress:
SETD.0 AsmSelOne
LDA.0
OUTA 0xE9
BRI assembleAddress
assembleByte:
SETD.0 TextValue
INCD.0
LDA.0
OUTA 0xE9
BRI assembleDone
assembleAddress:
SETD.0 TextValue
LDA.0
OUTA 0xE9
INCD.0
LDA.0
OUTA 0xE9
assembleDone:
; Past what was just written. The length is the shape's, out of the same table the
; disassembler reads, so the two can never disagree about how much room one takes.
SETD.0 ShapeLength
SETD.1 AsmShape
LDB.1
assembleStep:
BRB assembleStepped
INCD.0
DECB
BRI assembleStep
assembleStepped:
LDA.0
SETD.0 DumpAt
CALL stepCursor
RET
assembleUnknown:
SETD.0 NoSuchOp
CALL printString
CALL newLine
RET
assembleNeedsValue:
SETD.0 NeedsValue
CALL printString
CALL newLine
RET
; DP0 is a two byte address and A is how far to move it on.
stepCursor:
INCD.0
LDB.0
CCF
ADD
STQ.0
DECD.0
LDA.0
RSTB
ADD
STQ.0
RET
; The typed mnemonic into four padded characters and up to two selectors, which is the shape
; the table holds. Folded to upper case, because the assembler does not care about the case
; of a mnemonic and neither should this.
;
; A selector that was not typed is Data Pointer 0, exactly as an omitted one means in a
; source file. That is worth matching rather than demanding: half the instruction set names
; a pointer, and most code only ever uses the first.
takeMnemonic:
RSTA
SETD.1 AsmSelOne
STA.1
SETD.1 AsmSelTwo
STA.1
INIA 0d4
SETD.1 AsmLeft
STA.1
SETD.0 AsmLine
SETD.1 AsmName
takeMnemonicChar:
SETD.2 AsmLeft
LDA.2
BRA takeMnemonicPad ; Four is as long as a mnemonic gets.
LDA.0
BRA takeMnemonicPad ; The word ended.
INIB 0d46 ; A dot, so the selectors start here.
XOR
BRQ takeMnemonicPad
; Nothing below compares against A, so it survives all of this and is stored at the end.
INIB 0d97 ; a
CCF
SUB
BRC takeMnemonicPut ; It borrowed, so this is below 'a'.
INIB 0d123 ; One past z.
CCF
SUB
BNC takeMnemonicPut ; No borrow, so it is 'z' or later.
INIB 0x20
CCF
SUB
MVQA ; Upper case, which is how the table holds them.
takeMnemonicPut:
STA.1
INCD.0
INCD.1
SETD.2 AsmLeft
LDA.2
DECA
STA.2
BRI takeMnemonicChar
takeMnemonicPad:
SETD.2 AsmLeft
LDA.2
BRA takeMnemonicSelectors
INIA 0x20
STA.1
INCD.1
LDA.2
DECA
STA.2
BRI takeMnemonicPad
takeMnemonicSelectors:
RSTA
STA.1 ; The four are a string now, like the ones in the table.
LDA.0
INIB 0d46
XOR
BNQ takeMnemonicEnd ; No dot, so both selectors stay at nought.
INCD.0
LDA.0
INIB 0d48
CCF
SUB
MVQA
SETD.1 AsmSelOne
STA.1
INCD.0
LDA.0
INIB 0d46
XOR
BNQ takeMnemonicEnd
INCD.0
LDA.0
INIB 0d48
CCF
SUB
MVQA
SETD.1 AsmSelTwo
STA.1
takeMnemonicEnd:
RET
; Looks the four characters up. AsmOpcode and AsmShape are filled in and Q is zero, or Q is
; not zero and there is no such instruction.
;
; The same table the disassembler reads, searched the other way round. That is the point of
; it being a table rather than two lists: what can be written can be read back, and what can
; be read back can be written, and neither can drift from the other.
findByName:
SETD.3 Instructions
SETD.0 InstructionCount
LDA.0
SETD.1 AsmLeft
STA.1
findByNameStep:
PSHD.3
POPD.0
DPUP.0 0d02
SETD.2 AsmName
INIA 0d4
SETD.1 AsmCount
STA.1
findByNameChar:
LDA.0
LDB.2
XOR
BNQ findByNameNext
INCD.0
INCD.2
SETD.1 AsmCount
LDA.1
DECA
STA.1
BNA findByNameChar
PSHD.3
POPD.0
LDA.0
SETD.1 AsmOpcode
STA.1
PSHD.3
POPD.0
INCD.0
LDA.0
SETD.1 AsmShape
STA.1
RSTA
RSTB
CCF
ADD
RET
findByNameNext:
DPUP.3 0d07
SETD.1 AsmLeft
LDA.1
DECA
STA.1
BNA findByNameStep
RSTA
INIB 0d1
CCF
ADD
RET
; Is there such a bank? Q is zero if there is not.
;
; ASKING THE CONTROLLER FOR A BANK THAT IS NOT THERE IS REFUSED, and a refusal nobody
; catches stops the machine, which is a wretched answer to a mistyped number. The bank table
; says what exists, and it lives in bank 2.
;
; Bank n's record starts at n times eight. A and B are a shift register sixteen bits wide,
; so putting the number in the low half and rotating left three times multiplies it by
; eight without anything falling off the top: the most it can reach is 2040.
bankPresent:
RSTA
SETD.0 DumpBank
LDB.0
SHL SHL SHL
SETD.0 DumpRecord
STA.0
INCD.0
STB.0
INIA 0d2
OUTA 0xE0 ; SourceBank: the controller's own memory.
SETD.0 DumpRecord
LDA.0
OUTA 0xE1
INCD.0
LDA.0
OUTA 0xE2
INA 0xE9 ; The flags byte of that bank's record.
SETD.0 BankFlags
STA.0 ; Kept whole, since present is not the only thing it says.
INIB 0x01
AND
RET
; ---- help ----
doHelp:
SETD.0 HelpText
CALL printString
CALL newLine
SETD.0 HelpCdText
CALL printString
CALL newLine
SETD.0 HelpScriptText
CALL printString
CALL newLine
SETD.0 HelpMoreText
CALL printString
CALL newLine
; And what the monitor adds, but only when it is on. Listing commands that would not
; answer is a way of teaching somebody something untrue.
SETD.0 Mode
LDA.0
BRA prompt
SETD.0 MonitorHelp
CALL printString
CALL newLine
BRI prompt
#Data
Banner:
"CosmOS"
PromptText:
"> "
; Nothing has run yet, so nothing has failed yet.
; Whether the line the shell is on failed. Read by the script reader and by nothing else,
; which is why it is a plain flag and not a number: a script wants to know whether to go on,
; not what went wrong, and what went wrong has already been said in words.
LineFailed:
0x00
DriveWanted:
0x00
LastStatus:
0x00
OnFallback:
"this is the fallback: what boot.cfg asks for did not start
"
NoDisk:
"no filesystem on the disk"
ScriptUsage:
"do: give me the name of a script"
ScriptNoFile:
"do: cannot find it"
ScriptNotOne:
"do: that is not a script - it wants #! on the first line"
ScriptTooDeep:
"do: scripts are only four deep"
DriveNoSuch:
"drive: this machine has no such drive"
DriveNotReadable:
"drive: nothing this can read is in that drive"
StartupName:
"/System/Boot/startup.sh"
StartupNotOne:
"startup.sh is there but does not begin with #!, so it was not run"
ScriptStopped:
"stopped: that line did not work"
Unknown:
"I do not know: "
Farewell:
"halted"
DirectoryText:
"<dir>"
UnfinishedText:
"<unfinished>"
BlocksText:
" blocks"
NotDirectory:
"that is not a directory"
MadeText:
"made"
RemovedText:
"removed"
MkdirWhat:
"mkdir what?"
RmdirWhat:
"rmdir what?"
MkdirNo:
"cannot make that: check the path, the name, and whether it is taken"
RmdirNo:
"cannot remove that: it must be a directory, and empty"
Separator:
"/"
; Where a program is looked for when it is not where you are. One fixed place rather than a
; list somebody sets, because a list would need somewhere to live between one boot and the
; next, and there is no such place yet.
ScreenFileName:
"sbfs.screen"
AppsPrefix:
"/Apps/"
SystemAppsPrefix:
"0:/Apps/"
IsDirectory:
"that is a directory"
AndText:
", "
FoldersText:
" directories"
FolderText:
" directory"
FilesText:
" files"
FileText:
" file"
; Two strings rather than one, because a string literal stops at 255 characters and each
; one carries its own zero byte, so they are printed in turn rather than joined.
HelpText:
"dir list what is on the disk
load <file> read a program off the disk
run [words] start what was loaded, and tell it those words
<name> [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 <path> make a directory
rmdir <path> remove an empty one"
HelpScriptText:
"do <file> run the lines in a file, which must start with #!
echo [words] say them
clear empty the screen"
HelpMoreText:
"delete <file> take it off the disk
rename <file> <to> call it something else
monitor look at memory, change it, and jump into it
help this
exit stop, or leave the monitor if you are in it"
ExamineUsage:
"x <address>, or x on its own to carry on"
NoSuchBank:
"there is no such bank"
ProgramWord:
"program"
DataWord:
"data"
ExecMagic:
"SBEX"
; What every program on the disk is called, and the four characters that make one
; reachable by typing its name.
SbxSuffix:
".sbx"
LoadWhat:
"load what?"
NoSuchFile:
"no such file"
Deleted:
"gone"
Renamed:
"renamed"
DeleteWhat:
"delete what?"
RenameWhat:
"rename what to what?"
RenameNo:
"there is no such file, or that name is taken"
TooManyVectors:
"that program wants more vectors than there is room for"
Unreadable:
"could not read it"
NotProgram:
"not a program"
WrongVersion:
"a version I do not know"
LoadedText:
"loaded, starting at "
NothingLoaded:
"nothing is loaded"
Stopped:
"the program was stopped"
FaultOpcode:
"that byte is not an instruction"
FaultGuard:
"a write into a fenced off part of a bank"
FaultBank:
"a bank that is not there, or an address past its end"
FaultNoHandler:
"nothing is installed at service "
FaultNoDevice:
"nothing is installed for the device on port "
FaultAt:
", at "
FaultRegisters:
" A "
FaultB:
" B "
FaultQ:
" Q "
FaultSystem:
"that was the system itself, so there is nowhere to carry on from. Start the machine again."
Finished:
"finished"
BreakText:
"break at "
ARegText:
"A "
BRegText:
"B "
QRegText:
"Q "
SRegText:
"status "
DP0Text:
"DP0 "
DP1Text:
"DP1 "
DP2Text:
"DP2 "
DP3Text:
"DP3 "
SPText:
"SP "
CarryText:
"carry "
FaultText:
"fault "
IntsText:
"interrupts "
ResumeText:
"press a key "
MonitorPrompt:
"* "
UnknownText:
" ?
"
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 <program|data|number>"
BankIs:
"bank "
SetUsage:
"s <address> <byte> <byte> ..."
ReadOnlyText:
"that bank will not be written"
GoUsage:
"g <address>"
AsmName2:
"a"
AsmPrompt:
": "
DotText:
"."
AsmUsage:
"a <address>, 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
SbxText:
".sbx"
AppsPath:
"/Apps"
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
DiskReady:
0x00
LoadedOk:
0x00
; What loadProgram found, and the words for it. See loadProgram for what the numbers mean.
LoadStatus:
0x00
LoadMessage:
0x00 0x00
; THE PATH of the file to load, which load copies out of the line as typed and a typed
; program name is built into.
;
; Sixty four rather than the twenty three a NAME needs. It held a name when a disk was
; flat and there was nothing else to hold, and leaving it that size once paths existed cut
; every path longer than twenty two characters down to twenty two - which is not a failure
; that looks like one. "/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/",
; resolved perfectly well, and reported that the program was a directory.
ProgramName:
#Reserve 0d64
NameOk:
0x00
NamePrefix:
0x00
NameLeft:
0x00
LoadedEntry:
0x00 0x00
LoadCount:
0x00
LoadVersion:
0x00
; Where the first of rename's two names is, kept while the second is picked out of the
; line, since finding that needs the pointers for itself.
RenameFrom:
0x00 0x00
; What followed "run", kept for the program to ask for.
RunArgument:
#Reserve 0d64
; A number on its way to being printed, since the routine that prints one wants it in
; memory and a service is handed it in registers.
PrintNumber:
0x00 0x00
; ---- Where the last file anybody asked about lives ----
;
; osFileBlock is handed a name every time it is called, because a stateless service has no
; handle to leak and nothing left open by a program that stops in the middle. Taken at its
; word that means searching the directory once per block, so reading a four hundred block
; file walks the directory four hundred times over to be told the same thing.
;
; So the last answer is kept. A call naming the same file as the one before it skips the
; search and puts these back where sbfs keeps them. MEASURED, on the 329 block file the
; streaming test reads: 7% of the whole run saved when the file is the first entry in the
; directory, 11% when it is the sixteenth. Modest, and worth having for the shape rather
; than the size - what it really removes is a cost that grows with how full the disk is,
; on the one operation that is repeated once per block. NOTHING IS EVER TRUSTED THAT WAS NOT
; PUT HERE BY A SEARCH: this is a copy of an answer, not a second place where the truth
; about a file is written, and every path that could make it wrong calls fileForget. That
; is what makes it a speed rather than a promise - a cache that has been thrown away is
; indistinguishable from one that was never filled.
;
; The name is twenty three bytes for a name of twenty two, so that a name filling the
; field still has a zero after it and can be compared as a string.
FileCacheDrive:
0x00
SearchDrive:
0x00
RunDrive:
0x00
ScreenSaved:
0x00
ScreenWasDrive:
0x00
ScreenAt:
0x00
ScreenBlockAt:
0x00 0x00
ScreenBlock:
#Reserve 0d256
FileCacheValid:
0x00
FileCacheName:
#Reserve 0d64
FileCacheStart:
0x00 0x00
FileCacheBlocks:
0x00 0x00
FileCacheTail:
0x00
; ---- The vectors a loaded program brought with it ----
;
; Six bytes each: where it goes, what goes there, and what was there before. The last two
; are filled in when the program runs and read back when it exits, so what a program covers
; up comes back rather than becoming a hole.
;
; Sixteen is a limit rather than a considered number. It is far more than anything written
; so far wants, and a program asking for more is refused at load rather than having some of
; its handlers installed and the rest dropped.
VectorSource:
0x00 0x00
VectorsLeft:
0x00
LoadedVectorCount:
0x00
LoadedVectors:
#Reserve 0d96
; Where the monitor is looking, so that a bare 'dump' can carry on from it.
DumpBank:
0x00
DumpAt:
0x00 0x00
DumpRows:
0x00
Mode:
0x00
DumpCount:
0x00
BankWas:
0x00
BankFlags:
0x00
AsmSelOne:
0x00
AsmSelTwo:
0x00
AsmLeft:
0x00
AsmCount:
0x00
AsmOpcode:
0x00
AsmShape:
0x00
AsmName:
#Reserve 0d5
AsmLine:
#Reserve 0d41
ShowAsCode:
0x00
DumpRecord:
0x00 0x00
Opcode:
0x00
Shape:
0x00
Length:
0x00
InstrBytes:
#Reserve 0d4
; How many bytes an instruction of each shape runs to, the opcode included.
ShapeLength:
0d1 0d3 0d2 0d2 0d3 0d4 0d3
InstructionCount:
0d72
; ---- The instruction table ----
;
; Generated by Tests/instructiontable.py from the assembler's own list, and checked against
; it by Tests/docs.sh. Seven bytes each: the opcode, the shape, and four characters of name
; with the zero the assembler puts after a string.
Instructions:
0x10 0d0 "ADD "
0x11 0d0 "SUB "
0x12 0d0 "AND "
0x13 0d0 "OR "
0x14 0d0 "XOR "
0x15 0d0 "NOTA"
0x16 0d0 "NOTB"
0x17 0d0 "SHL "
0x18 0d0 "SHR "
0x60 0d1 "BRI "
0x61 0d1 "BRQ "
0x62 0d1 "BRA "
0x63 0d1 "BRB "
0x64 0d1 "BRC "
0x65 0d3 "BRD "
0x66 0d1 "BNQ "
0x67 0d1 "BNA "
0x68 0d1 "BNB "
0x69 0d1 "BNC "
0x70 0d1 "RCAL"
0x71 0d1 "CALL"
0x72 0d2 "SWI "
0x73 0d0 "RETI"
0x74 0d0 "RRET"
0x75 0d0 "RET "
0x76 0d0 "SRET"
0x20 0d0 "RSTA"
0x21 0d0 "RSTB"
0x22 0d0 "INCA"
0x23 0d0 "INCB"
0x24 0d0 "DECA"
0x25 0d0 "DECB"
0x26 0d2 "INIA"
0x27 0d2 "INIB"
0x28 0d0 "CCF "
0x29 0d0 "MVQA"
0x2A 0d0 "MVQB"
0x2B 0d0 "SIF "
0x2C 0d0 "CIF "
0x30 0d0 "PSHQ"
0x31 0d0 "PSHA"
0x32 0d0 "PSHB"
0x33 0d3 "PSHD"
0x34 0d0 "POPA"
0x35 0d0 "POPB"
0x36 0d3 "POPD"
0x40 0d3 "INCD"
0x41 0d3 "DECD"
0x42 0d3 "LDA "
0x43 0d3 "LDB "
0x44 0d3 "STQ "
0x45 0d3 "STA "
0x46 0d3 "STB "
0x47 0d5 "SETD"
0x48 0d4 "DPUP"
0x49 0d4 "DPDN"
0x4A 0d6 "LDD "
0x4B 0d6 "STD "
0x4C 0d3 "MVSD"
0x4D 0d3 "MVDS"
0x4E 0d3 "DPUA"
0x4F 0d3 "DPDA"
0x50 0d3 "DPUW"
0x51 0d3 "DPDW"
0xD0 0d2 "OUTQ"
0xD1 0d2 "OUTA"
0xD2 0d2 "OUTB"
0xE0 0d2 "INA "
0xE1 0d2 "INB "
0xF0 0d0 "NOP "
0xFE 0d0 "WAIT"
0xFF 0d0 "HALT"
DumpBytes:
#Reserve 0d16
; Where the system's Stack was when it handed the machine to a program. Kept below the
; region a program owns, so that a program has to go looking to break it.
SystemStack:
0x00 0x00
; 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
TabPrograms:
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
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
osBootState handleBootState
osBootSettle handleBootSettle
Device 0x20 diskDone