A five instruction program that writes one port and exits has no Data Segment at all, and the loader stopped the machine dead on it. It asked the memory controller to move a segment of no bytes, and a length of zero asks for the whole 64K - which is the machine's rule, and a reasonable one, since two bytes cannot say 65536 and a transfer of nothing is not usually what anybody meant. It is exactly what was meant here. 64K did not fit, the controller refused, and the load stopped half done. ON A TERMINAL THAT PRINTS A FAULT WITH AN ADDRESS. Behind a window it is a frozen picture and no reason at all, which is how it was found and is a separate problem from this one. The header says how long each segment is, so the loader knows before it asks. Both bytes are already in hand, so the test costs one OR. Nothing is lost by skipping the transfer: a blit leaves the controller's addresses past whatever it touched, and a blit of nothing would have left them where they already are, which is where the vectors are read from next. Guarded for the code segment too. A program with no code is equally assemblable and would have stopped in exactly the same place. Mode.sbx is the fix's test and a program worth having on its own: forty columns or eighty, whichever the screen is not in, which is what a person wanting Snake drawn twice the size actually needs. Ten instructions and no data, deliberately - it prints its two digits a register at a time rather than from a string, so it stays the smallest shape a loadable program can take. Nothing else on that disk had ever been that shape, which is why nothing had ever tried it. Reported by the user, who wrote the program. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
5372 lines
128 KiB
NASM
5372 lines
128 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 - 0x3FFF the system
|
|
; 0x4000 - a loaded program's code
|
|
; Data Memory 0x0000 - 0x1FFF the system
|
|
; 0x2000 - a loaded program's data
|
|
;
|
|
; Nothing enforces that. Nothing can: the fence guards a range, and this is a convention
|
|
; about which range belongs to whom rather than a rule about what may be touched. The
|
|
; assembler prints both segment sizes, and they are what to watch.
|
|
;
|
|
; A program is staged at 0x8000 while it is being loaded, which is inside the region a
|
|
; loaded program will own. That is safe because nothing is running during a load, and it
|
|
; is where a big program can be read without the system reserving the room for good.
|
|
;
|
|
; ---- What it can do ----
|
|
;
|
|
; dir List what is in the current directory.
|
|
; cd Go somewhere else, or to the root when told nothing.
|
|
; mkdir Make a directory. rmdir Remove an empty one.
|
|
; delete Remove a file. rename Give one another name.
|
|
; load Read a program off the disk and put it where it asks to go.
|
|
; run Start the program that was loaded.
|
|
; help Say what these are.
|
|
; exit Stop, or leave the monitor if that is where you are.
|
|
;
|
|
; Anything else that is not one of those is looked for as a program and run if it is
|
|
; found, so most of what the machine does is not in this list at all.
|
|
;
|
|
; The dispatch below is a chain of comparisons, which is the right shape for five commands
|
|
; and the wrong shape for twenty; it is eleven now, and when it grows the table that
|
|
; dispatchTest.asm demonstrates is where it should go.
|
|
;
|
|
; Written by Anachronaut
|
|
|
|
#Include console.asm
|
|
#Include text.asm
|
|
#Include sbfs.asm
|
|
#Include services.asm
|
|
#Include script.asm
|
|
|
|
#Program
|
|
|
|
boot:
|
|
; ---- The screen this system wants ----
|
|
;
|
|
; Eighty columns, because that is what CosmOS was written for: its own help text is
|
|
; seventy-four characters wide, and dir, the monitor and the assembler's messages all
|
|
; assume room. A machine wakes up in the smaller mode, which is right for a machine - it
|
|
; is the system that knows what shape of screen its own output needs.
|
|
;
|
|
; Harmless where there is no screen. Writing to a port nothing answers on does nothing,
|
|
; so this is one instruction wasted on a machine with a terminal instead.
|
|
INIA 0x01
|
|
OUTA 0x31
|
|
|
|
; And a cursor, so that a person can see where the next thing they type will go. A machine
|
|
; wakes up without one, which is right: a program painting its own screen does not want one
|
|
; blinking in the middle of it. A system that reads lines from a person does.
|
|
INIA 0x04
|
|
OUTA 0x02
|
|
|
|
SETD.0 Banner
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; Find out whether there is a filesystem to talk to. Doing this once at boot rather than
|
|
; once per command means a disk swapped underneath us is not noticed, which is honest
|
|
; for a machine whose disk is a file named on the command line.
|
|
CALL sbfsMountAll
|
|
SETD.0 DiskReady
|
|
BNQ bootNoDisk
|
|
INIA 0x01
|
|
STA.0
|
|
|
|
; ---- Saying that this start arrived ----
|
|
;
|
|
; The loader marks the disk before handing over, and nothing clears it but this. A system
|
|
; that crashes on the way here leaves the mark, and the loader finding it still set next
|
|
; time is how a machine that will not start says so.
|
|
;
|
|
; HERE RATHER THAN LATER, and the threshold is the whole of what the mark means. It is
|
|
; not a claim that anything works - a prompt can be reached by something broken in every
|
|
; other way. It is the point where somebody can type, which is what the fallback exists
|
|
; to give back: anything wrong past here can be fixed from the prompt, and nothing wrong
|
|
; before it can be fixed at all.
|
|
CALL sbfsBootState
|
|
BNQ bootStartup
|
|
SETD.0 SbfsStateWas
|
|
LDA.0
|
|
INIB 0d1
|
|
XOR
|
|
BRQ bootArrived
|
|
INIB 0d2
|
|
XOR
|
|
BNQ bootStartup
|
|
|
|
; Started by the fallback, so the system somebody asked for is not the one running. Said
|
|
; once, here, because there is nowhere else it would be noticed.
|
|
SETD.0 OnFallback
|
|
CALL printString
|
|
BRI bootStartup
|
|
|
|
bootArrived:
|
|
RSTA
|
|
CALL sbfsSetBootState
|
|
BRI bootStartup
|
|
bootNoDisk:
|
|
RSTA
|
|
STA.0
|
|
SETD.0 NoDisk
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; ---- Something to run before anybody types ----
|
|
;
|
|
; Every way of arriving at the prompt for the first time comes through here, which is the
|
|
; point: a startup script should run whether the disk was marked, unmarked, or is not there
|
|
; at all - and in the last case scriptOpen simply finds nothing.
|
|
;
|
|
; A MISSING ONE IS NOT A FAULT and says nothing, because a clean install has none and a
|
|
; machine that complained at every boot about a file nobody wrote would be teaching its owner
|
|
; to ignore it. A file that is THERE and is not a script is the other case entirely: somebody
|
|
; meant it to run.
|
|
bootStartup:
|
|
SETD.0 StartupName
|
|
CALL scriptOpen
|
|
BRQ bootReady
|
|
MVQA
|
|
INIB 0x01
|
|
CCF
|
|
SUB
|
|
BRQ bootReady ; There is none, which is ordinary.
|
|
SETD.0 StartupNotOne
|
|
CALL printString
|
|
CALL newLine
|
|
bootReady:
|
|
|
|
; ---- The loop ----
|
|
;
|
|
; The shell has two modes and one prompt that says which. Ordinary mode runs programs;
|
|
; monitor mode also looks at memory, changes it, and jumps into it.
|
|
;
|
|
; THE MODE IS A VARIABLE RATHER THAN A SECOND LOOP, and that is what makes it persistent
|
|
; without anything having to remember it. Every way back here goes through this one place,
|
|
; INCLUDING A PROGRAM GIVING THE MACHINE BACK - so jumping to an address, letting it run,
|
|
; and having it exit puts you back at the monitor prompt you left from, rather than at the
|
|
; shell. Only saying so leaves the monitor, or a program breaking the machine badly enough
|
|
; to need starting again.
|
|
prompt:
|
|
; ---- A script stops at the first line that did not work ----
|
|
;
|
|
; Checked here, before the next line is read, because this is the one place every command
|
|
; comes back to. A build whose first step failed and whose second step ran anyway produces
|
|
; something wrong and says it succeeded, which is the failure this whole flag exists to
|
|
; prevent.
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
BRA promptWhere
|
|
SETD.1 LineFailed
|
|
LDA.1
|
|
BRA promptWhere
|
|
SETD.0 ScriptStopped
|
|
CALL printString
|
|
CALL newLine
|
|
; Every level, not just this one. A build whose helper script failed should not carry on
|
|
; in the script that called the helper either.
|
|
CALL scriptAbandon
|
|
|
|
promptWhere:
|
|
; ---- The prompt is said by whoever turns out to be supplying the line ----
|
|
;
|
|
; Not here, which is where it was and where it cannot be right. The decision has to be made
|
|
; before the line is read and the answer is not known until after: a quiet script's #quiet
|
|
; is itself a line, so the prompt for it went out before anybody knew to stay silent, and
|
|
; the line AFTER a quiet script's last one comes from the console and had already been
|
|
; denied its prompt. Both were off by exactly one line and in opposite directions.
|
|
;
|
|
; So shellReadLine says it. It is the one place that knows.
|
|
SETD.0 CommandLine
|
|
INIB 0d127
|
|
CALL shellReadLine
|
|
|
|
; Running out of typing is how this ends. It is not the same as an empty line, which is
|
|
; just somebody pressing return, and the shell should sit there when that happens.
|
|
SETD.0 ConsoleEndOfInput
|
|
LDA.0
|
|
BNA quitRanOut
|
|
|
|
; ---- How this line went, assumed good until something says otherwise ----
|
|
;
|
|
; Cleared here rather than set at the end of each command, and that is what makes this
|
|
; affordable. There are thirty seven ways back to this prompt and only fourteen of them
|
|
; are failures, so marking the failures costs fourteen lines and marking the successes
|
|
; would cost twenty three - and the twenty three would have to be found again every time
|
|
; a command grew a new way to finish. A command that says nothing worked.
|
|
;
|
|
; A PROGRAM SETS THIS ITSELF, from handleExit, and it runs after this point - so what a
|
|
; program made of its work is what stands, not the zero written here before it started.
|
|
SETD.1 LineFailed
|
|
RSTA
|
|
STA.1
|
|
|
|
SETD.0 CommandLine
|
|
CALL textSplit
|
|
|
|
; An empty line asks for nothing.
|
|
SETD.0 CommandLine
|
|
LDA.0
|
|
BRA prompt
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DirName
|
|
CALL textSame
|
|
BRQ doDir
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 LoadName
|
|
CALL textSame
|
|
BRQ doLoad
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 RunName
|
|
CALL textSame
|
|
BRQ doRun
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 CdName
|
|
CALL textSame
|
|
BRQ doCd
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 MkdirName
|
|
CALL textSame
|
|
BRQ doMkdir
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 RmdirName
|
|
CALL textSame
|
|
BRQ doRmdir
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DeleteName
|
|
CALL textSame
|
|
BRQ doDelete
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 RenameName
|
|
CALL textSame
|
|
BRQ doRename
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DriveName
|
|
CALL textSame
|
|
BRQ doDrive
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ClearName
|
|
CALL textSame
|
|
BRQ doClear
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 EchoName
|
|
CALL textSame
|
|
BRQ doEcho
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DoName
|
|
CALL textSame
|
|
BRQ doScript
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 HelpName
|
|
CALL textSame
|
|
BRQ doHelp
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ExitName
|
|
CALL textSame
|
|
BRQ doExit
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 MonitorName
|
|
CALL textSame
|
|
BRQ doMonitor
|
|
|
|
; The monitor's own commands, which only answer when the monitor is on. They are single
|
|
; letters because they are typed constantly and because the prompt has already said which
|
|
; mode you are in; the plain shell keeps its words and stays plain.
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA promptUnknown
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ExamineName
|
|
CALL textSame
|
|
BRQ doExamine
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DisName
|
|
CALL textSame
|
|
BRQ doDisassemble
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 SetName
|
|
CALL textSame
|
|
BRQ doSet
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 BankName
|
|
CALL textSame
|
|
BRQ doBank
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 GoName
|
|
CALL textSame
|
|
BRQ doGo
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 AsmName2
|
|
CALL textSame
|
|
BRQ doAssemble
|
|
|
|
; 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.
|
|
;
|
|
; ---- 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:
|
|
SETD.1 EditBase
|
|
STD.0.1
|
|
SETD.1 EditRoom
|
|
STB.1
|
|
|
|
RSTA
|
|
SETD.1 EditLength
|
|
STA.1
|
|
SETD.1 EditAt
|
|
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
|
|
|
|
; Key mode, with the cursor the shell keeps. Nothing echoes from here on: everything that
|
|
; appears below is put there by this routine.
|
|
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
|
|
|
|
; Anything else is a character if it is printable and nothing at all if it is not. The
|
|
; console's own keys that this shell has no use for land here and are ignored rather than
|
|
; typed, which is the whole reason they are above ASCII.
|
|
INIB 0x20
|
|
CCF
|
|
SUB
|
|
BRC editKey
|
|
INIB 0x7F
|
|
CCF
|
|
SUB
|
|
BNC editKey
|
|
|
|
; ---- Putting a character in ----
|
|
editInsert:
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditRoom
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNC editKey ; Full. The zero byte on the end is not counted in the room.
|
|
|
|
; A hole at the insertion point, made from the top down so nothing is overwritten before
|
|
; it has been moved.
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditAt
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
MVQB ; How many characters are above the insertion point.
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
DPUA.0 ; One past the last character.
|
|
editInsertShift:
|
|
BRB editInsertPut
|
|
DECD.0
|
|
LDA.0
|
|
INCD.0
|
|
STA.0
|
|
DECD.0
|
|
DECB
|
|
BRI editInsertShift
|
|
editInsertPut:
|
|
SETD.1 EditChar
|
|
LDA.1
|
|
STA.0
|
|
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
|
|
; At the end of the line, printing it IS the change.
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNQ editInsertRedraw
|
|
SETD.1 EditChar
|
|
LDA.1
|
|
OUTA 0x00
|
|
BRI editKey
|
|
editInsertRedraw:
|
|
CALL editRedraw
|
|
BRI editKey
|
|
|
|
; ---- Taking one out ----
|
|
;
|
|
; Backspace and Delete are different keys doing different things: one takes the character
|
|
; BEFORE the insertion point, the other the one under it. They meet at editTakeOut, which is
|
|
; the only part they share.
|
|
editBack:
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
BRA editKey ; Nothing before it. Rubbing out past the start of the line
|
|
; would eat the prompt, which belongs to whoever printed it.
|
|
DECA
|
|
STA.1
|
|
|
|
; Whether that was the last character decides how it disappears, and it has to be asked
|
|
; before the buffer shortens under it.
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
DECB
|
|
CCF
|
|
SUB
|
|
BNQ editBackMiddle
|
|
|
|
CALL editTakeOut
|
|
; Rubbed out where it stands, which is what a terminal has always done for a backspace: no
|
|
; cursor moved by hand, and so nothing said to the terminal but three ordinary bytes.
|
|
INIA 0x08
|
|
OUTA 0x00
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
INIA 0x08
|
|
OUTA 0x00
|
|
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
|
|
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
|
|
editFinish:
|
|
|
|
; Line mode, with the cursor still on: that is how boot left the console and how every
|
|
; program that knows nothing of any of this expects to find it.
|
|
INIA 0x04
|
|
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:
|
|
; One space after it. A line that has just got shorter has a character left over on the
|
|
; end, and printing over it costs less than working out whether there is one.
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
|
|
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 EditLength
|
|
LDA.1
|
|
INCA ; The space was printed too.
|
|
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
|
|
|
|
; ---- 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.
|
|
RET
|
|
|
|
; Video memory as bank 4, which is what makes it reachable at all. Bank 3 is the disk's; see
|
|
; the table in the CosmOS README, which exists because a program once took 3.
|
|
screenBank:
|
|
INIA 0d4
|
|
OUTA 0xE3
|
|
INIA 0x30
|
|
OUTA 0xE2
|
|
INIA 0x03
|
|
OUTA 0xE8
|
|
RET
|
|
|
|
; Back to the drive whoever called was standing on.
|
|
screenGoBack:
|
|
SETD.0 ScreenWasDrive
|
|
LDA.0
|
|
CALL sbfsUse
|
|
RET
|
|
|
|
; ---- Which page of video memory a saved block is ----
|
|
;
|
|
; Nought to 191 are the tiles and the map, which sit next to each other from 0x0000. After
|
|
; that comes 16K of nothing, so 192 to 195 jump to 0xFC and are the palette. One sum rather
|
|
; than two loops, because two loops is two places to get the file's block numbers wrong.
|
|
screenPageFor:
|
|
INIB 0d192
|
|
CCF
|
|
SUB
|
|
BRC screenPageDirect ; Borrowed, so it is below 192 and the page is the index.
|
|
MVQA
|
|
INIB 0xFC
|
|
CCF
|
|
ADD
|
|
RET
|
|
screenPageDirect:
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; A holds a page of video memory. Its 256 bytes come into ScreenBlock.
|
|
screenFromVideo:
|
|
PSHA
|
|
INIA 0d4
|
|
OUTA 0xE0
|
|
POPA
|
|
OUTA 0xE1
|
|
RSTA
|
|
OUTA 0xE2
|
|
INIA 0x01
|
|
OUTA 0xE3 ; Into Data Memory.
|
|
CALL screenBufferDest
|
|
CALL screenLength
|
|
INIA 0x01
|
|
OUTA 0xE8
|
|
RET
|
|
|
|
; A holds a page of video memory. ScreenBlock goes back into it.
|
|
screenToVideo:
|
|
PSHA
|
|
INIA 0d4
|
|
OUTA 0xE3
|
|
POPA
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
INIA 0x01
|
|
OUTA 0xE0 ; Out of Data Memory.
|
|
CALL screenBufferSource
|
|
CALL screenLength
|
|
INIA 0x01
|
|
OUTA 0xE8
|
|
RET
|
|
|
|
; Where ScreenBlock is, told to the controller. A Data Pointer's two bytes cannot be read out
|
|
; of it, so it goes to memory first and comes back a byte at a time.
|
|
screenBufferDest:
|
|
SETD.1 ScreenBlockAt
|
|
SETD.0 ScreenBlock
|
|
STD.0.1
|
|
LDA.1
|
|
OUTA 0xE4
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE5
|
|
RET
|
|
|
|
screenBufferSource:
|
|
SETD.1 ScreenBlockAt
|
|
SETD.0 ScreenBlock
|
|
STD.0.1
|
|
LDA.1
|
|
OUTA 0xE1
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE2
|
|
RET
|
|
|
|
screenLength:
|
|
INIA 0x01
|
|
OUTA 0xE6
|
|
RSTA
|
|
OUTA 0xE7 ; 0x0100, which is one block.
|
|
RET
|
|
|
|
; The block, emptied. Everything the register block below does not fill has to be nought, or
|
|
; a screen restored would carry whatever the last file read left in here.
|
|
screenClearBlock:
|
|
SETD.0 ScreenBlock
|
|
RSTB
|
|
screenClearByte:
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BNB screenClearByte
|
|
RET
|
|
|
|
; ---- run ----
|
|
;
|
|
; Hands the machine to whatever was loaded. Where the Stack is now is written down first,
|
|
; because the program is not going to unwind anything it pushes and the exit handler has
|
|
; to be able to put the Stack back.
|
|
|
|
doRun:
|
|
SETD.0 LoadedOk
|
|
LDA.0
|
|
BRA runNothing
|
|
|
|
; Where typing a program's name arrives, having loaded it on the way. The argument comes
|
|
; out of TextRest either way: after "run" that is what followed the word, and after a
|
|
; program's own name it is what followed the name, which is the same thing meaning the
|
|
; same thing.
|
|
runLoaded:
|
|
INA 0x24
|
|
SETD.0 RunDrive
|
|
STA.0
|
|
MVSD.0
|
|
SETD.1 SystemStack
|
|
STD.0.1
|
|
|
|
; And where the machine is, so that the exit handler has something to put back.
|
|
SETD.0 SbfsCwd
|
|
SETD.1 SavedCwd
|
|
CALL sbfsCopyWord
|
|
|
|
; Whatever followed the word "run" is kept where the program can ask for it. Copied
|
|
; rather than pointed at, because what it is pointing at is the line the shell typed
|
|
; into, and a program is entitled to outlive the shell's opinion of that.
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
SETD.1 RunArgument
|
|
INIB 0d64
|
|
CALL copyText
|
|
|
|
CALL installVectors
|
|
|
|
; The entry address is a number until BRD makes it a place. DP3 is the one to build it
|
|
; in, because it is the pointer nothing puts back.
|
|
SETD.1 LoadedEntry
|
|
LDD.3.1
|
|
BRD.3
|
|
|
|
runNothing:
|
|
SETD.0 NothingLoaded
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; DP0 is a string, DP1 is where it should go, and B is how much room there is counting
|
|
; the zero on the end. What does not fit is left behind, and what is written is a string
|
|
; either way.
|
|
copyText:
|
|
BRB copyTextDone ; No room at all, so nothing is written, not even the zero.
|
|
copyTextLoop:
|
|
DECB
|
|
BRB copyTextEnd ; Only room for the terminator now.
|
|
LDA.0
|
|
STA.1
|
|
BRA copyTextDone
|
|
INCD.0
|
|
INCD.1
|
|
BRI copyTextLoop
|
|
copyTextEnd:
|
|
RSTA
|
|
STA.1
|
|
copyTextDone:
|
|
RET
|
|
|
|
; ---- Putting a program's vectors in, and taking them out again ----
|
|
;
|
|
; The vector table lives in Program Memory, which no instruction can write, so both of
|
|
; these go through the memory controller. Port 0xE9 reads a byte from the source and writes
|
|
; a byte to the destination, stepping the address on either way, so a two byte entry is two
|
|
; reads or two writes and no address arithmetic in between.
|
|
;
|
|
; What was in the slot is kept before anything replaces it, and put back afterwards, rather
|
|
; than the slot being cleared. Clearing would be wrong wherever a program has installed a
|
|
; handler over one the system was already using: the program is allowed to do that, and
|
|
; when it goes, what it covered up has to come back rather than becoming a hole.
|
|
|
|
installVectors:
|
|
SETD.0 LoadedVectorCount
|
|
LDA.0
|
|
BRA installDone
|
|
SETD.1 VectorsLeft
|
|
STA.1
|
|
SETD.3 LoadedVectors
|
|
|
|
installOne:
|
|
; DP3 walks one six byte entry: where it goes, what goes there, and room for what was
|
|
; there before. Reading and writing the same slot, so the controller is pointed at it
|
|
; from both ends at once and the address is only worked out once.
|
|
RSTA
|
|
OUTA 0xE0 ; SourceBank: Program Memory.
|
|
OUTA 0xE3 ; DestBank: the same.
|
|
LDA.3
|
|
OUTA 0xE1
|
|
OUTA 0xE4
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE2
|
|
OUTA 0xE5
|
|
INCD.3 ; On the handler.
|
|
|
|
; What is there now, before anything replaces it.
|
|
INA 0xE9
|
|
PSHA
|
|
INA 0xE9
|
|
PSHA
|
|
|
|
; And the handler in its place.
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3 ; On the two bytes kept for what was there before.
|
|
|
|
; The Stack gives them back in the reverse of the order they went on, so the low byte
|
|
; arrives first and is written to the second of the two. Getting this the natural way
|
|
; round instead put the low byte where the high one goes and the high byte over the
|
|
; handler, which the first run of a program survives - the table is already written by
|
|
; then - and the second run does not.
|
|
POPA
|
|
INCD.3
|
|
STA.3
|
|
DECD.3
|
|
POPA
|
|
STA.3
|
|
INCD.3
|
|
INCD.3
|
|
|
|
SETD.1 VectorsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA installOne
|
|
installDone:
|
|
RET
|
|
|
|
removeVectors:
|
|
SETD.0 LoadedVectorCount
|
|
LDA.0
|
|
BRA removeDone
|
|
SETD.1 VectorsLeft
|
|
STA.1
|
|
SETD.3 LoadedVectors
|
|
|
|
removeOne:
|
|
RSTA
|
|
OUTA 0xE3 ; DestBank: Program Memory.
|
|
LDA.3
|
|
OUTA 0xE4
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE5
|
|
INCD.3
|
|
INCD.3
|
|
INCD.3 ; Past the handler, to what was underneath it.
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
|
|
SETD.1 VectorsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA removeOne
|
|
removeDone:
|
|
RET
|
|
|
|
; ---- The services ----
|
|
;
|
|
; These are what a loaded program is allowed to ask for. The names and their numbers come
|
|
; from services.asm, which the programs include as well, so neither side writes a number
|
|
; down and the two cannot disagree about them.
|
|
;
|
|
; A handler arrives with the caller's registers exactly as they were: an interrupt frame
|
|
; is pushed, not cleared. So the pointer a program put in DP0 is still there to be used.
|
|
|
|
; The disk finishing, acknowledged and ignored.
|
|
;
|
|
; The system drives the disk by asking its status port and waiting, so it has no use for
|
|
; the line. But the disk raises one after every operation whether anybody wants it or not,
|
|
; and a line goes on waiting while the Interrupt Flag is down rather than being lost. The
|
|
; shell keeps the flag down, so the line from the last disk read was still standing when
|
|
; the first program to enable interrupts ran, and it arrived there - a fault, in a program
|
|
; that had never heard of the disk, blamed on the innocent instruction that let it through.
|
|
;
|
|
; Answering a line is what takes it down, so this is one instruction and that is the point.
|
|
diskDone:
|
|
RETI
|
|
|
|
handlePrintString:
|
|
CALL printString
|
|
RETI
|
|
|
|
handleReadLine:
|
|
CALL readLine
|
|
|
|
; 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
|
|
|
|
SETD.0 Finished
|
|
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"
|
|
Finished:
|
|
"finished"
|
|
|
|
BreakText:
|
|
"break at "
|
|
ARegText:
|
|
"A "
|
|
BRegText:
|
|
"B "
|
|
QRegText:
|
|
"Q "
|
|
SRegText:
|
|
"status "
|
|
DP0Text:
|
|
"DP0 "
|
|
DP1Text:
|
|
"DP1 "
|
|
DP2Text:
|
|
"DP2 "
|
|
DP3Text:
|
|
"DP3 "
|
|
SPText:
|
|
"SP "
|
|
CarryText:
|
|
"carry "
|
|
FaultText:
|
|
"fault "
|
|
IntsText:
|
|
"interrupts "
|
|
ResumeText:
|
|
"press a key "
|
|
MonitorPrompt:
|
|
"* "
|
|
UnknownText:
|
|
" ?
|
|
"
|
|
MonitorName:
|
|
"monitor"
|
|
MonitorHelp:
|
|
"x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves"
|
|
ExamineName:
|
|
"x"
|
|
DisName:
|
|
"d"
|
|
SetName:
|
|
"s"
|
|
BankName:
|
|
"b"
|
|
GoName:
|
|
"g"
|
|
BankUsage:
|
|
"b <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"
|
|
DirName:
|
|
"dir"
|
|
DoName:
|
|
"do"
|
|
EchoName:
|
|
"echo"
|
|
ClearName:
|
|
"clear"
|
|
DriveName:
|
|
"drive"
|
|
LoadName:
|
|
"load"
|
|
RunName:
|
|
"run"
|
|
DeleteName:
|
|
"delete"
|
|
RenameName:
|
|
"rename"
|
|
CdName:
|
|
"cd"
|
|
MkdirName:
|
|
"mkdir"
|
|
RmdirName:
|
|
"rmdir"
|
|
HelpName:
|
|
"help"
|
|
ExitName:
|
|
"exit"
|
|
|
|
; Where the machine is, written out for the prompt, and where in the buffer it begins.
|
|
; Built from the end backwards, so it starts somewhere in the middle.
|
|
CwdText:
|
|
#Reserve 0d127
|
|
CwdAt:
|
|
0x00 0x00
|
|
CwdWalk:
|
|
0x00 0x00
|
|
PathLength:
|
|
0x00
|
|
|
|
; What is left of CwdText to write into, and whether the path ran out of it. Counted down
|
|
; rather than compared against an address, because the check is on every byte written and
|
|
; a subtraction of two pointers is a great deal more than a byte that is already going to
|
|
; be tested for zero.
|
|
CwdRoom:
|
|
0x00
|
|
CwdCut:
|
|
0x00
|
|
|
|
; What the working directory was when a program was started, so that it can be put back
|
|
; when the program stops.
|
|
SavedCwd:
|
|
0x00 0x00
|
|
|
|
DirFolders:
|
|
0x00
|
|
DirTaken:
|
|
0x00
|
|
DiskReady:
|
|
0x00
|
|
LoadedOk:
|
|
0x00
|
|
|
|
; What loadProgram found, and the words for it. See loadProgram for what the numbers mean.
|
|
LoadStatus:
|
|
0x00
|
|
LoadMessage:
|
|
0x00 0x00
|
|
|
|
; THE PATH of the file to load, which load copies out of the line as typed and a typed
|
|
; program name is built into.
|
|
;
|
|
; Sixty four rather than the twenty three a NAME needs. It held a name when a disk was
|
|
; flat and there was nothing else to hold, and leaving it that size once paths existed cut
|
|
; every path longer than twenty two characters down to twenty two - which is not a failure
|
|
; that looks like one. "/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/",
|
|
; resolved perfectly well, and reported that the program was a directory.
|
|
ProgramName:
|
|
#Reserve 0d64
|
|
NameOk:
|
|
0x00
|
|
NamePrefix:
|
|
0x00
|
|
NameLeft:
|
|
0x00
|
|
LoadedEntry:
|
|
0x00 0x00
|
|
LoadCount:
|
|
0x00
|
|
LoadVersion:
|
|
0x00
|
|
|
|
; Where the first of rename's two names is, kept while the second is picked out of the
|
|
; line, since finding that needs the pointers for itself.
|
|
RenameFrom:
|
|
0x00 0x00
|
|
|
|
; What followed "run", kept for the program to ask for.
|
|
RunArgument:
|
|
#Reserve 0d64
|
|
|
|
; A number on its way to being printed, since the routine that prints one wants it in
|
|
; memory and a service is handed it in registers.
|
|
PrintNumber:
|
|
0x00 0x00
|
|
|
|
; ---- Where the last file anybody asked about lives ----
|
|
;
|
|
; osFileBlock is handed a name every time it is called, because a stateless service has no
|
|
; handle to leak and nothing left open by a program that stops in the middle. Taken at its
|
|
; word that means searching the directory once per block, so reading a four hundred block
|
|
; file walks the directory four hundred times over to be told the same thing.
|
|
;
|
|
; So the last answer is kept. A call naming the same file as the one before it skips the
|
|
; search and puts these back where sbfs keeps them. MEASURED, on the 329 block file the
|
|
; streaming test reads: 7% of the whole run saved when the file is the first entry in the
|
|
; directory, 11% when it is the sixteenth. Modest, and worth having for the shape rather
|
|
; than the size - what it really removes is a cost that grows with how full the disk is,
|
|
; on the one operation that is repeated once per block. NOTHING IS EVER TRUSTED THAT WAS NOT
|
|
; PUT HERE BY A SEARCH: this is a copy of an answer, not a second place where the truth
|
|
; about a file is written, and every path that could make it wrong calls fileForget. That
|
|
; is what makes it a speed rather than a promise - a cache that has been thrown away is
|
|
; indistinguishable from one that was never filled.
|
|
;
|
|
; The name is twenty three bytes for a name of twenty two, so that a name filling the
|
|
; field still has a zero after it and can be compared as a string.
|
|
FileCacheDrive:
|
|
0x00
|
|
SearchDrive:
|
|
0x00
|
|
RunDrive:
|
|
0x00
|
|
ScreenSaved:
|
|
0x00
|
|
ScreenWasDrive:
|
|
0x00
|
|
ScreenAt:
|
|
0x00
|
|
ScreenBlockAt:
|
|
0x00 0x00
|
|
ScreenBlock:
|
|
#Reserve 0d256
|
|
FileCacheValid:
|
|
0x00
|
|
FileCacheName:
|
|
#Reserve 0d64
|
|
FileCacheStart:
|
|
0x00 0x00
|
|
FileCacheBlocks:
|
|
0x00 0x00
|
|
FileCacheTail:
|
|
0x00
|
|
|
|
; ---- The vectors a loaded program brought with it ----
|
|
;
|
|
; Six bytes each: where it goes, what goes there, and what was there before. The last two
|
|
; are filled in when the program runs and read back when it exits, so what a program covers
|
|
; up comes back rather than becoming a hole.
|
|
;
|
|
; Sixteen is a limit rather than a considered number. It is far more than anything written
|
|
; so far wants, and a program asking for more is refused at load rather than having some of
|
|
; its handlers installed and the rest dropped.
|
|
VectorSource:
|
|
0x00 0x00
|
|
VectorsLeft:
|
|
0x00
|
|
LoadedVectorCount:
|
|
0x00
|
|
LoadedVectors:
|
|
#Reserve 0d96
|
|
|
|
; Where the monitor is looking, so that a bare 'dump' can carry on from it.
|
|
DumpBank:
|
|
0x00
|
|
DumpAt:
|
|
0x00 0x00
|
|
DumpRows:
|
|
0x00
|
|
Mode:
|
|
0x00
|
|
DumpCount:
|
|
0x00
|
|
BankWas:
|
|
0x00
|
|
BankFlags:
|
|
0x00
|
|
AsmSelOne:
|
|
0x00
|
|
AsmSelTwo:
|
|
0x00
|
|
AsmLeft:
|
|
0x00
|
|
AsmCount:
|
|
0x00
|
|
AsmOpcode:
|
|
0x00
|
|
AsmShape:
|
|
0x00
|
|
AsmName:
|
|
#Reserve 0d5
|
|
AsmLine:
|
|
#Reserve 0d41
|
|
ShowAsCode:
|
|
0x00
|
|
DumpRecord:
|
|
0x00 0x00
|
|
Opcode:
|
|
0x00
|
|
Shape:
|
|
0x00
|
|
Length:
|
|
0x00
|
|
InstrBytes:
|
|
#Reserve 0d4
|
|
|
|
; How many bytes an instruction of each shape runs to, the opcode included.
|
|
ShapeLength:
|
|
0d1 0d3 0d2 0d2 0d3 0d4 0d3
|
|
|
|
InstructionCount:
|
|
0d72
|
|
|
|
; ---- The instruction table ----
|
|
;
|
|
; Generated by Tests/instructiontable.py from the assembler's own list, and checked against
|
|
; it by Tests/docs.sh. Seven bytes each: the opcode, the shape, and four characters of name
|
|
; with the zero the assembler puts after a string.
|
|
Instructions:
|
|
0x10 0d0 "ADD "
|
|
0x11 0d0 "SUB "
|
|
0x12 0d0 "AND "
|
|
0x13 0d0 "OR "
|
|
0x14 0d0 "XOR "
|
|
0x15 0d0 "NOTA"
|
|
0x16 0d0 "NOTB"
|
|
0x17 0d0 "SHL "
|
|
0x18 0d0 "SHR "
|
|
0x60 0d1 "BRI "
|
|
0x61 0d1 "BRQ "
|
|
0x62 0d1 "BRA "
|
|
0x63 0d1 "BRB "
|
|
0x64 0d1 "BRC "
|
|
0x65 0d3 "BRD "
|
|
0x66 0d1 "BNQ "
|
|
0x67 0d1 "BNA "
|
|
0x68 0d1 "BNB "
|
|
0x69 0d1 "BNC "
|
|
0x70 0d1 "RCAL"
|
|
0x71 0d1 "CALL"
|
|
0x72 0d2 "SWI "
|
|
0x73 0d0 "RETI"
|
|
0x74 0d0 "RRET"
|
|
0x75 0d0 "RET "
|
|
0x76 0d0 "SRET"
|
|
0x20 0d0 "RSTA"
|
|
0x21 0d0 "RSTB"
|
|
0x22 0d0 "INCA"
|
|
0x23 0d0 "INCB"
|
|
0x24 0d0 "DECA"
|
|
0x25 0d0 "DECB"
|
|
0x26 0d2 "INIA"
|
|
0x27 0d2 "INIB"
|
|
0x28 0d0 "CCF "
|
|
0x29 0d0 "MVQA"
|
|
0x2A 0d0 "MVQB"
|
|
0x2B 0d0 "SIF "
|
|
0x2C 0d0 "CIF "
|
|
0x30 0d0 "PSHQ"
|
|
0x31 0d0 "PSHA"
|
|
0x32 0d0 "PSHB"
|
|
0x33 0d3 "PSHD"
|
|
0x34 0d0 "POPA"
|
|
0x35 0d0 "POPB"
|
|
0x36 0d3 "POPD"
|
|
0x40 0d3 "INCD"
|
|
0x41 0d3 "DECD"
|
|
0x42 0d3 "LDA "
|
|
0x43 0d3 "LDB "
|
|
0x44 0d3 "STQ "
|
|
0x45 0d3 "STA "
|
|
0x46 0d3 "STB "
|
|
0x47 0d5 "SETD"
|
|
0x48 0d4 "DPUP"
|
|
0x49 0d4 "DPDN"
|
|
0x4A 0d6 "LDD "
|
|
0x4B 0d6 "STD "
|
|
0x4C 0d3 "MVSD"
|
|
0x4D 0d3 "MVDS"
|
|
0x4E 0d3 "DPUA"
|
|
0x4F 0d3 "DPDA"
|
|
0x50 0d3 "DPUW"
|
|
0x51 0d3 "DPDW"
|
|
0xD0 0d2 "OUTQ"
|
|
0xD1 0d2 "OUTA"
|
|
0xD2 0d2 "OUTB"
|
|
0xE0 0d2 "INA "
|
|
0xE1 0d2 "INB "
|
|
0xF0 0d0 "NOP "
|
|
0xFE 0d0 "WAIT"
|
|
0xFF 0d0 "HALT"
|
|
DumpBytes:
|
|
#Reserve 0d16
|
|
|
|
; Where the system's Stack was when it handed the machine to a program. Kept below the
|
|
; region a program owns, so that a program has to go looking to break it.
|
|
SystemStack:
|
|
0x00 0x00
|
|
DirSeen:
|
|
0x00
|
|
DirSize:
|
|
0x00 0x00
|
|
WidthLeft:
|
|
0x00
|
|
|
|
; ---- What a line being typed is made of ----
|
|
;
|
|
; All of it lives here rather than being passed about, because the routine that reads a line
|
|
; is a loop over keystrokes and there are four Data Pointers, two of which it needs for the
|
|
; buffer and the walking.
|
|
EditBase:
|
|
0x00 0x00
|
|
EditRoom:
|
|
0x00
|
|
EditLength:
|
|
0x00
|
|
EditAt:
|
|
0x00
|
|
EditRow:
|
|
0x00
|
|
EditColumn:
|
|
0x00
|
|
EditWidth:
|
|
0x00
|
|
EditChar:
|
|
0x00
|
|
|
|
; Where the counting up and down the rows has got to. Separate from EditRow and EditColumn,
|
|
; which are where the LINE starts: one of the two things that reads these is working out
|
|
; that very answer, and a walk that wrote its intermediate steps into its own starting point
|
|
; would be walking away from a moving mark.
|
|
EditWalkRow:
|
|
0x00
|
|
EditWalkColumn:
|
|
0x00
|
|
EditWalkBack:
|
|
0x00
|
|
|
|
; 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
|
|
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
|