Everything drawn on this machine was in a cell. Something between two cells meant rewriting both; something moving a pixel at a time meant rewriting them sixty times a second, which is affordable for one thing and not for twenty. A sprite is put at a pixel and the device draws it over whatever is behind, so moving it costs two bytes. MADE OF TILES, which is the decision the rest follows from: m by n taken in reading order from one index, so there is no second pixel format, no second kind of memory, and nothing a sprite can show that the map cannot. A 16 by 16 character is four tiles and the background can name the same four. 256 entries of 8 bytes at 0xC000 in the atlas - eight so the entry address is a shift, the same no-multiply argument as the palette's four. Position is signed and sixteen bits, because 640 by 400 does not fit in a byte and a sprite has to be able to sit half off the left rather than appearing whole at the edge. A PIXEL OF ZERO IS NOT DRAWN, or every sprite is a rectangle. Tested before the attribute is added, so a hole belongs to the art and not to the colour scheme. The same rule the other way round is what "behind" means: drawn only where the background pixel was zero, so a thing walks behind a pillar and in front of the floor in one frame. All of them draw, every frame, so they cannot flicker. Real machines dropped them per scanline because they had a fixed number of shift registers; this has a loop. The limit is the size of the table, which is a constant rather than a property of what is on screen. And the system takes them down at exit. The sprite table sits in the gap the screen save walks around - to the end of the map, then the palette - and that is right, because nothing the shell draws is a sprite: there is nothing to give back, only something to take away. Otherwise a program that put a ball up and left would leave it over the prompt, in front of everything, with nothing able to type it away. Sprite.asm deliberately leaves its own, because a program that faulted could not have cleared it. Every check here was re-broken and failed: transparency, reading order, draw order, priority, and size. Size needed breaking twice - the first attempt did not compile, and a silent build failure had left the old binary passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
8243 lines
196 KiB
NASM
8243 lines
196 KiB
NASM
; cosmos.asm
|
|
; CosmOS, and the shell that is most of it.
|
|
;
|
|
; The machine boots into this. It registers what the hardware brought, mounts whatever
|
|
; disk is attached, and then reads lines and does what they say until there is no more
|
|
; typing to be had.
|
|
;
|
|
; ---- Where things live ----
|
|
;
|
|
; The system keeps to the bottom of both memories, and everything above is for whatever
|
|
; it is running:
|
|
;
|
|
; Program Memory 0x0000 - 0x4FFF the system
|
|
; 0x5000 - a loaded program's code
|
|
; Data Memory 0x0000 - 0x2FFF the system
|
|
; 0x3000 - a loaded program's data
|
|
;
|
|
; Nothing enforces that. Nothing can: the fence guards a range, and this is a convention
|
|
; about which range belongs to whom rather than a rule about what may be touched. The
|
|
; assembler prints both segment sizes, and they are what to watch.
|
|
;
|
|
; A program is staged at 0x8000 while it is being loaded, which is inside the region a
|
|
; loaded program will own. That is safe because nothing is running during a load, and it
|
|
; is where a big program can be read without the system reserving the room for good.
|
|
;
|
|
; ---- What it can do ----
|
|
;
|
|
; dir List what is in the current directory.
|
|
; cd Go somewhere else, or to the root when told nothing.
|
|
; mkdir Make a directory. rmdir Remove an empty one.
|
|
; delete Remove a file. rename Give one another name.
|
|
; load Read a program off the disk and put it where it asks to go.
|
|
; run Start the program that was loaded.
|
|
; help Say what these are.
|
|
; exit Stop, or leave the monitor if that is where you are.
|
|
;
|
|
; Anything else that is not one of those is looked for as a program and run if it is
|
|
; found, so most of what the machine does is not in this list at all.
|
|
;
|
|
; The dispatch below is a chain of comparisons, which is the right shape for five commands
|
|
; and the wrong shape for twenty; it is eleven now, and when it grows the table that
|
|
; dispatchTest.asm demonstrates is where it should go.
|
|
;
|
|
; Written by Anachronaut
|
|
|
|
#Include console.asm
|
|
#Include text.asm
|
|
#Include sbfs.asm
|
|
#Include services.asm
|
|
#Include script.asm
|
|
|
|
#Program
|
|
|
|
boot:
|
|
; ---- The screen this system wants ----
|
|
;
|
|
; Eighty columns, because that is what CosmOS was written for: its own help text is
|
|
; seventy-four characters wide, and dir, the monitor and the assembler's messages all
|
|
; assume room. A machine wakes up in the smaller mode, which is right for a machine - it
|
|
; is the system that knows what shape of screen its own output needs.
|
|
;
|
|
; Harmless where there is no screen. Writing to a port nothing answers on does nothing,
|
|
; so this is one instruction wasted on a machine with a terminal instead.
|
|
INIA 0x01
|
|
OUTA 0x31
|
|
|
|
; And a cursor, so that a person can see where the next thing they type will go. A machine
|
|
; wakes up without one, which is right: a program painting its own screen does not want one
|
|
; blinking in the middle of it. A system that reads lines from a person does.
|
|
INIA 0x04
|
|
OUTA 0x02
|
|
|
|
SETD.0 Banner
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; Find out whether there is a filesystem to talk to. Doing this once at boot rather than
|
|
; once per command means a disk swapped underneath us is not noticed, which is honest
|
|
; for a machine whose disk is a file named on the command line.
|
|
CALL sbfsMountAll
|
|
SETD.0 DiskReady
|
|
BNQ bootNoDisk
|
|
INIA 0x01
|
|
STA.0
|
|
|
|
; ---- Saying that this start arrived ----
|
|
;
|
|
; The loader marks the disk before handing over, and nothing clears it but this. A system
|
|
; that crashes on the way here leaves the mark, and the loader finding it still set next
|
|
; time is how a machine that will not start says so.
|
|
;
|
|
; HERE RATHER THAN LATER, and the threshold is the whole of what the mark means. It is
|
|
; not a claim that anything works - a prompt can be reached by something broken in every
|
|
; other way. It is the point where somebody can type, which is what the fallback exists
|
|
; to give back: anything wrong past here can be fixed from the prompt, and nothing wrong
|
|
; before it can be fixed at all.
|
|
CALL sbfsBootState
|
|
BNQ bootStartup
|
|
SETD.0 SbfsStateWas
|
|
LDA.0
|
|
INIB 0d1
|
|
XOR
|
|
BRQ bootArrived
|
|
INIB 0d2
|
|
XOR
|
|
BNQ bootStartup
|
|
|
|
; Started by the fallback, so the system somebody asked for is not the one running. Said
|
|
; once, here, because there is nowhere else it would be noticed.
|
|
SETD.0 OnFallback
|
|
CALL printString
|
|
BRI bootStartup
|
|
|
|
bootArrived:
|
|
RSTA
|
|
CALL sbfsSetBootState
|
|
BRI bootStartup
|
|
bootNoDisk:
|
|
RSTA
|
|
STA.0
|
|
SETD.0 NoDisk
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; ---- Something to run before anybody types ----
|
|
;
|
|
; Every way of arriving at the prompt for the first time comes through here, which is the
|
|
; point: a startup script should run whether the disk was marked, unmarked, or is not there
|
|
; at all - and in the last case scriptOpen simply finds nothing.
|
|
;
|
|
; A MISSING ONE IS NOT A FAULT and says nothing, because a clean install has none and a
|
|
; machine that complained at every boot about a file nobody wrote would be teaching its owner
|
|
; to ignore it. A file that is THERE and is not a script is the other case entirely: somebody
|
|
; meant it to run.
|
|
bootStartup:
|
|
SETD.0 StartupName
|
|
CALL scriptOpen
|
|
BRQ bootReady
|
|
MVQA
|
|
INIB 0x01
|
|
CCF
|
|
SUB
|
|
BRQ bootReady ; There is none, which is ordinary.
|
|
SETD.0 StartupNotOne
|
|
CALL printString
|
|
CALL newLine
|
|
bootReady:
|
|
|
|
; ---- Where the Stack is when nothing is happening ----
|
|
;
|
|
; Taken once, here, and put back at the top of every turn of the loop below. SystemStack is
|
|
; not this: that is taken when a PROGRAM starts, to be given back when it stops, so it
|
|
; holds wherever the shell had got to at that moment - which is the value that needs
|
|
; correcting rather than the one to correct from.
|
|
MVSD.0
|
|
SETD.1 ShellStack
|
|
STD.0.1
|
|
|
|
; ---- The loop ----
|
|
;
|
|
; The shell has two modes and one prompt that says which. Ordinary mode runs programs;
|
|
; monitor mode also looks at memory, changes it, and jumps into it.
|
|
;
|
|
; THE MODE IS A VARIABLE RATHER THAN A SECOND LOOP, and that is what makes it persistent
|
|
; without anything having to remember it. Every way back here goes through this one place,
|
|
; INCLUDING A PROGRAM GIVING THE MACHINE BACK - so jumping to an address, letting it run,
|
|
; and having it exit puts you back at the monitor prompt you left from, rather than at the
|
|
; shell. Only saying so leaves the monitor, or a program breaking the machine badly enough
|
|
; to need starting again.
|
|
prompt:
|
|
; ---- The Stack, back where it was when nothing was happening ----
|
|
;
|
|
; EVERY FAILURE IN THIS SHELL ABANDONS A FRAME. commandFailed is reached with CALL and
|
|
; never returns: it marks the line and branches here, which is the idiom every command
|
|
; uses and is why a failure needs no unwinding. What it costs is the frame of that call and
|
|
; of everything between here and it - twenty bytes for a name that was never set, more from
|
|
; somewhere deeper - and nothing ever gave them back. Twenty failed lines moved the Stack
|
|
; Pointer from FFFD to FE6D, and it only ever went one way.
|
|
;
|
|
; Nothing had noticed because it takes thousands of failures to reach anything, and nobody
|
|
; types thousands of anything. A loop in a script would.
|
|
;
|
|
; So the loop starts each turn from a known place. This is the second use of MVDS in the
|
|
; system and it earns it for the same reason as the first: a Stack that is right by
|
|
; construction beats one that is right by everybody remembering.
|
|
SETD.1 ShellStack
|
|
LDD.0.1
|
|
MVDS.0
|
|
|
|
; ---- What an "if" line's command made of it ----
|
|
;
|
|
; Read here because this is where every command comes back to, and the last place a result
|
|
; is still the result of the line that produced it. It has to happen BEFORE the stop on
|
|
; failure below: a condition that fails is the ordinary half of a question, not a script
|
|
; going wrong, so the flag is cleared as it is taken.
|
|
SETD.1 IfPending
|
|
LDA.1
|
|
BRA promptNoIf
|
|
RSTA
|
|
STA.1
|
|
SETD.1 LineFailed
|
|
LDA.1
|
|
BRA promptIfTaken
|
|
INIA 0d1 ; It failed, so this branch is not taken and an else would be.
|
|
BRI promptIfPush
|
|
promptIfTaken:
|
|
RSTA
|
|
promptIfPush:
|
|
CALL blockPush
|
|
BNQ ifDeep
|
|
|
|
; A loop has to be able to come back to the line that asked, which if does not.
|
|
SETD.1 LoopKind
|
|
LDA.1
|
|
BRA promptIfPlain
|
|
CALL blockTop
|
|
BNQ promptIfPlain
|
|
CALL blockKeepWhere
|
|
promptIfPlain:
|
|
RSTA
|
|
SETD.1 LineFailed
|
|
STA.1 ; And the question is answered rather than unanswered.
|
|
promptNoIf:
|
|
|
|
; ---- A script stops at the first line that did not work ----
|
|
;
|
|
; Checked here, before the next line is read, because this is the one place every command
|
|
; comes back to. A build whose first step failed and whose second step ran anyway produces
|
|
; something wrong and says it succeeded, which is the failure this whole flag exists to
|
|
; prevent.
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
BRA promptWhere
|
|
SETD.1 LineFailed
|
|
LDA.1
|
|
BRA promptWhere
|
|
SETD.0 ScriptStopped
|
|
CALL printString
|
|
CALL newLine
|
|
; Every level, not just this one. A build whose helper script failed should not carry on
|
|
; in the script that called the helper either.
|
|
CALL scriptAbandon
|
|
|
|
promptWhere:
|
|
; ---- The prompt is said by whoever turns out to be supplying the line ----
|
|
;
|
|
; Not here, which is where it was and where it cannot be right. The decision has to be made
|
|
; before the line is read and the answer is not known until after: a quiet script's #quiet
|
|
; is itself a line, so the prompt for it went out before anybody knew to stay silent, and
|
|
; the line AFTER a quiet script's last one comes from the console and had already been
|
|
; denied its prompt. Both were off by exactly one line and in opposite directions.
|
|
;
|
|
; So shellReadLine says it. It is the one place that knows.
|
|
SETD.0 CommandLine
|
|
INIB 0d127
|
|
CALL shellReadLine
|
|
|
|
; Running out of typing is how this ends. It is not the same as an empty line, which is
|
|
; just somebody pressing return, and the shell should sit there when that happens.
|
|
SETD.0 ConsoleEndOfInput
|
|
LDA.0
|
|
BNA quitRanOut
|
|
|
|
; ---- How this line went, assumed good until something says otherwise ----
|
|
;
|
|
; Cleared here rather than set at the end of each command, and that is what makes this
|
|
; affordable. There are thirty seven ways back to this prompt and only fourteen of them
|
|
; are failures, so marking the failures costs fourteen lines and marking the successes
|
|
; would cost twenty three - and the twenty three would have to be found again every time
|
|
; a command grew a new way to finish. A command that says nothing worked.
|
|
;
|
|
; A PROGRAM SETS THIS ITSELF, from handleExit, and it runs after this point - so what a
|
|
; program made of its work is what stands, not the zero written here before it started.
|
|
SETD.1 LineFailed
|
|
RSTA
|
|
STA.1
|
|
|
|
; ---- Names filled in before anybody reads the line ----
|
|
;
|
|
; Here rather than in the script reader, so that a typed line and a line out of a file
|
|
; behave the same and no command below has to know that variables exist. A line that names
|
|
; something nothing was ever set to does not run at all: it says so and counts as failed,
|
|
; which is what makes a mistyped name a complaint instead of an empty path.
|
|
;
|
|
; THERE IS NOTHING TO TEST AFTERWARDS, and a test was written here before that was noticed:
|
|
; commandFailed does not return. It marks the line and branches to the prompt, the way every
|
|
; command in this shell reports a failure, so the only way out of the expansion is the one
|
|
; where it worked.
|
|
; ---- Room to indent ----
|
|
;
|
|
; Leading spaces are taken off, which the shell never allowed and never needed to: a line
|
|
; was a command and a command started at the front. Blocks change that. Nobody writes an if
|
|
; inside an if without indenting what is inside them, and a line that began with a space
|
|
; used to split into an empty first word and match nothing at all.
|
|
CALL lineTrim
|
|
|
|
; ---- And a line nobody is running is not run ----
|
|
;
|
|
; Before the names are filled in, on purpose. A branch that is not being taken must not be
|
|
; able to fail, and a name it mentions has no business existing.
|
|
CALL blockSkipping
|
|
BNQ blockPassOver
|
|
|
|
CALL varExpand
|
|
|
|
promptDispatch:
|
|
SETD.0 CommandLine
|
|
CALL textSplit
|
|
|
|
; An empty line asks for nothing.
|
|
SETD.0 CommandLine
|
|
LDA.0
|
|
BRA prompt
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DirName
|
|
CALL textSame
|
|
BRQ doDir
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 LoadName
|
|
CALL textSame
|
|
BRQ doLoad
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 RunName
|
|
CALL textSame
|
|
BRQ doRun
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 CdName
|
|
CALL textSame
|
|
BRQ doCd
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 MkdirName
|
|
CALL textSame
|
|
BRQ doMkdir
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 RmdirName
|
|
CALL textSame
|
|
BRQ doRmdir
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DeleteName
|
|
CALL textSame
|
|
BRQ doDelete
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 RenameName
|
|
CALL textSame
|
|
BRQ doRename
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DriveName
|
|
CALL textSame
|
|
BRQ doDrive
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ClearName
|
|
CALL textSame
|
|
BRQ doClear
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 EchoName
|
|
CALL textSame
|
|
BRQ doEcho
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DoName
|
|
CALL textSame
|
|
BRQ doScript
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 SetVarName
|
|
CALL textSame
|
|
BRQ doSetVar
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 IfName
|
|
CALL textSame
|
|
BRQ doIf
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ElseName
|
|
CALL textSame
|
|
BRQ doElse
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 EndName
|
|
CALL textSame
|
|
BRQ doEnd
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 SameName
|
|
CALL textSame
|
|
BRQ doSame
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 WhileName
|
|
CALL textSame
|
|
BRQ doWhile
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ForName
|
|
CALL textSame
|
|
BRQ doFor
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 HelpName
|
|
CALL textSame
|
|
BRQ doHelp
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ExitName
|
|
CALL textSame
|
|
BRQ doExit
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 MonitorName
|
|
CALL textSame
|
|
BRQ doMonitor
|
|
|
|
; The monitor's own commands, which only answer when the monitor is on. They are single
|
|
; letters because they are typed constantly and because the prompt has already said which
|
|
; mode you are in; the plain shell keeps its words and stays plain.
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA promptUnknown
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ExamineName
|
|
CALL textSame
|
|
BRQ doExamine
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 DisName
|
|
CALL textSame
|
|
BRQ doDisassemble
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 SetName
|
|
CALL textSame
|
|
BRQ doSet
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 BankName
|
|
CALL textSame
|
|
BRQ doBank
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 GoName
|
|
CALL textSame
|
|
BRQ doGo
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 AsmName2
|
|
CALL textSame
|
|
BRQ doAssemble
|
|
|
|
; ---- And a word the monitor does not know either ----
|
|
;
|
|
; It goes where the shell's unknown words go, which is the disk, because the monitor is a
|
|
; MODE of the shell rather than a different program and everything the shell can do is
|
|
; still supposed to work in it.
|
|
;
|
|
; THERE WAS NOTHING HERE AT ALL, and the fall through was into sayPrompt - a routine, which
|
|
; ended in a RET that had nothing of its own to return to. So it took whatever the Stack
|
|
; held and went there. Sometimes that was 0x0003, in the middle of newLine, and the machine
|
|
; stopped on a byte that is not an instruction; once it was inside sbfsFormat, and the
|
|
; machine formatted the disk it had booted from. The tell was a SECOND prompt printed
|
|
; before the wreckage, which is sayPrompt doing exactly what it is for on its way past.
|
|
BRI promptUnknown
|
|
|
|
; Where you are, and then the prompt itself. A routine rather than a run of code in the loop,
|
|
; because the loop is no longer the only thing that needs it - see shellReadTyped.
|
|
sayPrompt:
|
|
; Where you are, but only when that is not obvious. At the root the prompt is the one it
|
|
; has always been, so a machine nobody has moved about on looks exactly as it did - and
|
|
; every recorded test that never says "cd" keeps its recorded prompt.
|
|
SETD.0 SbfsCwd
|
|
LDA.0
|
|
INCD.0
|
|
LDB.0
|
|
OR
|
|
BRQ sayPromptMode
|
|
CALL shellPath
|
|
SETD.0 CwdAt
|
|
LDD.0.0
|
|
CALL printString
|
|
|
|
sayPromptMode:
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA sayPromptPlain
|
|
SETD.0 MonitorPrompt
|
|
BRI sayPromptSay
|
|
sayPromptPlain:
|
|
SETD.0 PromptText
|
|
sayPromptSay:
|
|
CALL printString
|
|
RET
|
|
|
|
; ---- Where a line comes from ----
|
|
;
|
|
; The whole of what a script is. Everything below this - splitting the line, matching it
|
|
; against the commands, loading a program - cannot tell the difference and does not have to.
|
|
;
|
|
; A SCRIPT RUNNING OUT IS NOT THE SAME AS TYPING RUNNING OUT. The console ending means there
|
|
; is nobody there and the shell should stop; a script ending means go back to whoever asked
|
|
; for it. So the end of a script falls through to the console rather than to the door.
|
|
shellReadLine:
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
BRA shellReadTyped
|
|
CALL scriptLine
|
|
BNQ shellReadLine ; That one ended. Ask again: something under it may still be
|
|
; running, and only depth reaching nought means the console.
|
|
|
|
; The prompt and the echo are one thing - together they look like somebody typing - so a
|
|
; quiet script gets neither, and everything else gets both in that order.
|
|
SETD.1 ScriptQuiet
|
|
LDA.1
|
|
BNA shellReadQuiet
|
|
CALL sayPrompt
|
|
CALL printString
|
|
CALL newLine
|
|
shellReadQuiet:
|
|
RET
|
|
|
|
shellReadTyped:
|
|
CALL sayPrompt
|
|
CALL editLine
|
|
RET
|
|
|
|
; ---- The same, for a caller that prints a prompt of its own ----
|
|
;
|
|
; The monitor's assembler shows the address it is about to write to and then asks for a line,
|
|
; so it must not also get the shell's. It still wants the line to come from the script when
|
|
; there is one, which is what lets a script carry a block of assembly.
|
|
shellReadRaw:
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
BRA shellReadRawTyped
|
|
CALL scriptLine
|
|
BNQ shellReadRaw
|
|
SETD.1 ScriptQuiet
|
|
LDA.1
|
|
BNA shellReadRawQuiet
|
|
CALL printString
|
|
CALL newLine
|
|
shellReadRawQuiet:
|
|
RET
|
|
shellReadRawTyped:
|
|
CALL editLine
|
|
RET
|
|
|
|
; ---- The line the person at the keyboard is typing ----
|
|
;
|
|
; THREE DIFFERENT THINGS USED TO DO THIS JOB and which one you got depended on where the
|
|
; machine was running. On a terminal the host held the line and did the echoing and the
|
|
; backspacing; behind a window the console's own gatherer did it; from a file nothing did it
|
|
; at all. One job, three implementations, and none of them here - which is why there was no
|
|
; way to move about in a line and nowhere for a history to live.
|
|
;
|
|
; So the shell does it. The console delivers keys and says nothing about what they mean, the
|
|
; same way the disk says what a drive is and nothing about what should be on it. Where the
|
|
; cursor goes and what the line looks like afterwards are decisions, and decisions belong to
|
|
; whoever is reading.
|
|
;
|
|
; DP0 names a buffer and B says how many characters it holds, not counting the zero byte
|
|
; that ends it. Q is how long the line turned out to be. That is readLine's bargain exactly,
|
|
; so this drops in where that was called.
|
|
;
|
|
; ConsoleEndOfInput is set if the console ran out instead of a line being finished, which is
|
|
; readLine's other promise and the one the shell uses to know when to stop.
|
|
;
|
|
; ---- Two doors, because the history is the SHELL'S ----
|
|
;
|
|
; editLine is what the shell reads through and keeps what it is given. editLinePlain is what
|
|
; a program reads through, and does not: Edit would otherwise fill the history with the text
|
|
; of somebody's document, and pressing Up in the middle of writing one would put "dir" in
|
|
; it. A program gets the editing, which is what it wanted; the history belongs to the thing
|
|
; whose lines are commands.
|
|
;
|
|
; Two entry points rather than a flag a caller sets first, because a caller cannot forget to
|
|
; do this one.
|
|
;
|
|
; ---- Why most keystrokes draw nothing but themselves ----
|
|
;
|
|
; A character typed at the END of a line needs no cursor moved: printing it is the whole of
|
|
; the change. That is the case almost every keystroke is, and it matters beyond speed -
|
|
; moving the cursor by hand means writing the console's cursor registers, which a terminal
|
|
; is told about in an escape sequence. Redrawing on every keypress would fill every recorded
|
|
; transcript in the test suite with them. So the cheap path is the common one, and the line
|
|
; is only reprinted when something happened in the middle of it.
|
|
editLine:
|
|
INIA 0x01
|
|
BRI editLineBegin
|
|
editLinePlain:
|
|
RSTA
|
|
editLineBegin:
|
|
SETD.1 EditKeepHistory
|
|
STA.1
|
|
|
|
SETD.1 EditBase
|
|
STD.0.1
|
|
SETD.1 EditRoom
|
|
STB.1
|
|
|
|
RSTA
|
|
SETD.1 EditLength
|
|
STA.1
|
|
SETD.1 EditAt
|
|
STA.1
|
|
SETD.1 EditDrawn
|
|
STA.1
|
|
SETD.1 ConsoleEndOfInput
|
|
STA.1
|
|
|
|
; Where the line begins, ASKED rather than assumed. The prompt has just been printed and
|
|
; only the console knows where it left off.
|
|
INA 0x03
|
|
SETD.1 EditRow
|
|
STA.1
|
|
INA 0x04
|
|
SETD.1 EditColumn
|
|
STA.1
|
|
|
|
; How wide the screen is, which is what decides where a long line carries on. A machine
|
|
; with nothing on that port answers zero, and a width of zero would make the walking below
|
|
; never finish, so it is taken as the width this system asks for at boot.
|
|
INA 0x32
|
|
BNA editWidthKnown
|
|
INIA 0d80
|
|
editWidthKnown:
|
|
SETD.1 EditWidth
|
|
STA.1
|
|
|
|
; ---- How the console was found, so it can be put back that way ----
|
|
;
|
|
; Not "line mode with a cursor", which is only how the SHELL keeps it. A program that had
|
|
; asked for key mode and then read a line through the system would have been handed back a
|
|
; console in line mode, having asked for nothing of the sort.
|
|
;
|
|
; The status port reports every one of the three things the control port can ask for, and
|
|
; reports them in the same order two bits along - so one shift turns what the console IS
|
|
; into what to write to make it that again.
|
|
INA 0x01
|
|
RSTB
|
|
SHR
|
|
SHR
|
|
INIB 0d7
|
|
AND
|
|
MVQA
|
|
SETD.1 EditWasControl
|
|
STA.1
|
|
|
|
; Key mode with a cursor, and INTERRUPTS OFF whatever they were. Nothing echoes from here
|
|
; on: everything that appears below is put there by this routine. Interrupts are off
|
|
; because this is about to block on the data port, and the manual is explicit that a
|
|
; program does one or the other and not both.
|
|
INIA 0x05
|
|
OUTA 0x02
|
|
|
|
editKey:
|
|
INA 0x00
|
|
SETD.1 EditChar
|
|
STA.1
|
|
|
|
; The ways out first, since they leave rather than change anything. XOR leaves the answer
|
|
; in Q and A alone, so one read stands for the whole ladder.
|
|
INIB 0x0A
|
|
XOR
|
|
BRQ editDone
|
|
INIB 0x0D
|
|
XOR
|
|
BRQ editDone
|
|
INIB 0xFF
|
|
XOR
|
|
BRQ editEnded
|
|
|
|
; ---- And the key that means there is no more ----
|
|
;
|
|
; Ctrl-D, which the terminal used to turn into the end of input all by itself and cannot any
|
|
; more: that is a thing a terminal does while it is holding the line, and it is not holding
|
|
; it now. So the shell does it, which is the same trade as the echoing and the backspacing.
|
|
;
|
|
; ONLY ON AN EMPTY LINE, which is the rule everywhere else it appears. In the middle of
|
|
; something typed it means neither "stop" nor a character, so it means nothing.
|
|
INIB 0x04
|
|
XOR
|
|
BNQ editNotEnd
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
BRB editEnded
|
|
editNotEnd:
|
|
|
|
INIB 0x08
|
|
XOR
|
|
BRQ editBack
|
|
INIB 0x86
|
|
XOR
|
|
BRQ editDelete
|
|
INIB 0x82
|
|
XOR
|
|
BRQ editLeft
|
|
INIB 0x83
|
|
XOR
|
|
BRQ editRight
|
|
INIB 0x84
|
|
XOR
|
|
BRQ editHome
|
|
INIB 0x85
|
|
XOR
|
|
BRQ editEnd
|
|
INIB 0x80
|
|
XOR
|
|
BRQ editUp
|
|
INIB 0x81
|
|
XOR
|
|
BRQ editDown
|
|
INIB 0x09
|
|
XOR
|
|
BRQ tabComplete
|
|
|
|
; Anything else is a character if it is printable and nothing at all if it is not. The
|
|
; console's own keys that this shell has no use for land here and are ignored rather than
|
|
; typed, which is the whole reason they are above ASCII.
|
|
INIB 0x20
|
|
CCF
|
|
SUB
|
|
BRC editKey
|
|
INIB 0x7F
|
|
CCF
|
|
SUB
|
|
BNC editKey
|
|
|
|
; ---- Putting a character in ----
|
|
editInsert:
|
|
CALL editPut
|
|
BNQ editKey ; No room for it. The zero byte on the end is not counted.
|
|
|
|
; At the end of the line, printing it IS the change. The insertion point is READ BACK
|
|
; rather than still being in A: it was, while this was one run of code, and a RET puts A
|
|
; back to whatever the caller had.
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNQ editInsertRedraw
|
|
SETD.1 EditChar
|
|
LDA.1
|
|
OUTA 0x00
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditDrawn
|
|
STA.1
|
|
BRI editKey
|
|
editInsertRedraw:
|
|
CALL editRedraw
|
|
BRI editKey
|
|
|
|
; Puts the character in EditChar into the line where the insertion point is, and moves the
|
|
; insertion point past it. DRAWS NOTHING: what that should look like depends on where in the
|
|
; line it went, and the caller is what knows. Q is zero if it went in and one if there was
|
|
; no room.
|
|
;
|
|
; A routine rather than the run of code it used to be, because Tab completing a word puts in
|
|
; several characters and every one of them is this.
|
|
editPut:
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditRoom
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNC editPutFull
|
|
|
|
; A hole at the insertion point, made from the top down so nothing is overwritten before
|
|
; it has been moved.
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditAt
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
MVQB ; How many characters are above the insertion point.
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
DPUA.0 ; One past the last character.
|
|
editPutShift:
|
|
BRB editPutHere
|
|
DECD.0
|
|
LDA.0
|
|
INCD.0
|
|
STA.0
|
|
DECD.0
|
|
DECB
|
|
BRI editPutShift
|
|
editPutHere:
|
|
SETD.1 EditChar
|
|
LDA.1
|
|
STA.0
|
|
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: it went in.
|
|
RET
|
|
editPutFull:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is one: it did not.
|
|
RET
|
|
|
|
; ---- Taking one out ----
|
|
;
|
|
; Backspace and Delete are different keys doing different things: one takes the character
|
|
; BEFORE the insertion point, the other the one under it. They meet at editTakeOut, which is
|
|
; the only part they share.
|
|
editBack:
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
BRA editKey ; Nothing before it. Rubbing out past the start of the line
|
|
; would eat the prompt, which belongs to whoever printed it.
|
|
DECA
|
|
STA.1
|
|
|
|
; Whether that was the last character decides how it disappears, and it has to be asked
|
|
; before the buffer shortens under it.
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
DECB
|
|
CCF
|
|
SUB
|
|
BNQ editBackMiddle
|
|
|
|
CALL editTakeOut
|
|
; Rubbed out where it stands, which is what a terminal has always done for a backspace: no
|
|
; cursor moved by hand, and so nothing said to the terminal but three ordinary bytes.
|
|
INIA 0x08
|
|
OUTA 0x00
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
INIA 0x08
|
|
OUTA 0x00
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditDrawn
|
|
STA.1
|
|
BRI editKey
|
|
editBackMiddle:
|
|
CALL editTakeOut
|
|
CALL editRedraw
|
|
BRI editKey
|
|
|
|
editDelete:
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ editKey ; Nothing under the cursor at the end of a line.
|
|
CALL editTakeOut
|
|
CALL editRedraw
|
|
BRI editKey
|
|
|
|
; Takes the character at the insertion point out of the buffer and shortens it. Draws
|
|
; nothing: what that should look like is the caller's business and the two callers disagree.
|
|
editTakeOut:
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
DPUA.0
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditAt
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
MVQB
|
|
DECB ; One of those is the character going.
|
|
editTakeOutShift:
|
|
BRB editTakeOutDone
|
|
INCD.0
|
|
LDA.0
|
|
DECD.0
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BRI editTakeOutShift
|
|
editTakeOutDone:
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
RET
|
|
|
|
; ---- Moving about in it ----
|
|
;
|
|
; Nothing on the screen changes, so nothing on the screen is redrawn: only the cursor moves.
|
|
editLeft:
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
BRA editKey
|
|
DECA
|
|
STA.1
|
|
BRI editShow
|
|
|
|
editRight:
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ editKey ; Already at the end.
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI editShow
|
|
|
|
editHome:
|
|
RSTA
|
|
SETD.1 EditAt
|
|
STA.1
|
|
BRI editShow
|
|
|
|
editEnd:
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditAt
|
|
STA.1
|
|
BRI editShow
|
|
|
|
; ---- Backwards and forwards through what was typed before ----
|
|
;
|
|
; The line on the screen is replaced outright, so both of these redraw. That is also what
|
|
; makes the padding below matter: a long line replaced by a short one leaves the tail of the
|
|
; long one behind unless something rubs it out.
|
|
editUp:
|
|
SETD.1 EditKeepHistory
|
|
LDA.1
|
|
BRA editKey ; A program's line has no history behind it.
|
|
SETD.1 HistoryPick
|
|
LDA.1
|
|
BRA editKey ; Already at the oldest one kept.
|
|
|
|
; Leaving the line being typed for the first time, so it is put somewhere. Down brings it
|
|
; back, and a person who pressed Up to look at something gets their line returned rather
|
|
; than taken.
|
|
SETD.1 HistoryCount
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNQ editUpMoving
|
|
SETD.3 HistoryTyped
|
|
CALL historyPut
|
|
editUpMoving:
|
|
SETD.1 HistoryPick
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
CALL historySlot
|
|
BRI editRecall
|
|
|
|
editDown:
|
|
SETD.1 EditKeepHistory
|
|
LDA.1
|
|
BRA editKey ; A program's line has no history behind it.
|
|
SETD.1 HistoryPick
|
|
LDA.1
|
|
SETD.1 HistoryCount
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ editKey ; Already back at the line being typed.
|
|
SETD.1 HistoryPick
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
SETD.1 HistoryCount
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ editDownTyped
|
|
SETD.1 HistoryPick
|
|
LDA.1
|
|
CALL historySlot
|
|
BRI editRecall
|
|
editDownTyped:
|
|
SETD.3 HistoryTyped
|
|
editRecall:
|
|
CALL historyTake
|
|
CALL editRedraw
|
|
BRI editKey
|
|
|
|
editShow:
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
CALL editPlace
|
|
BRI editKey
|
|
|
|
; ---- The end of a line, and the end of the typing ----
|
|
editEnded:
|
|
INIA 0x01
|
|
SETD.1 ConsoleEndOfInput
|
|
STA.1
|
|
; AND NO NEWLINE. Nobody pressed Return, so there is no line to end: the console simply
|
|
; stopped having anything to say. Printing one here pushes whatever is said next down a row
|
|
; for a reason nobody could see, which is what it did until this line was written.
|
|
BRI editFinish
|
|
editDone:
|
|
; The newline belongs after the whole line, not after wherever the cursor was left sitting.
|
|
; Asked first, because a line finished at its end - which is almost every line - needs no
|
|
; cursor moved and so says nothing to the terminal.
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ editDoneEnd
|
|
LDA.1
|
|
CALL editPlace
|
|
editDoneEnd:
|
|
CALL newLine
|
|
|
|
; Kept before the buffer is handed over and while EditLength still says how long it is.
|
|
; Only on this path: a console that ran out was not somebody finishing a line.
|
|
SETD.1 EditKeepHistory
|
|
LDA.1
|
|
BRA editFinish
|
|
CALL historyAdd
|
|
editFinish:
|
|
|
|
; And the console back exactly as it was found.
|
|
SETD.1 EditWasControl
|
|
LDA.1
|
|
OUTA 0x02
|
|
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
DPUA.0
|
|
RSTA
|
|
STA.0 ; The zero byte that ends it.
|
|
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is how long the line is.
|
|
RET
|
|
|
|
; ---- Putting the line back on the screen ----
|
|
;
|
|
; The whole of it, every time, rather than the part that changed. A hundred and twenty odd
|
|
; characters is nothing to this machine, and the alternative is four separate cases that all
|
|
; have to agree about what is already up there.
|
|
editRedraw:
|
|
SETD.1 EditRow
|
|
LDA.1
|
|
OUTA 0x03
|
|
SETD.1 EditColumn
|
|
LDA.1
|
|
OUTA 0x04
|
|
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
editRedrawNext:
|
|
BRB editRedrawTail
|
|
LDA.0
|
|
OUTA 0x00
|
|
INCD.0
|
|
DECB
|
|
BRI editRedrawNext
|
|
editRedrawTail:
|
|
; ---- Rubbing out what is no longer there ----
|
|
;
|
|
; One space was enough while the only thing that ever shortened a line was taking one
|
|
; character out of it. Pressing Up replaces the whole line, and a long one replaced by a
|
|
; short one leaves the tail of the long one sitting on the screen looking like part of
|
|
; what you are typing.
|
|
;
|
|
; So the spaces go on for exactly as far as the line has shrunk, which is one after a
|
|
; character was taken out and none at all when one was put in. printSpaces takes none for
|
|
; an answer and prints nothing, which is what makes that case cost nothing to allow.
|
|
SETD.1 EditDrawn
|
|
LDA.1
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRC editRedrawGrew ; Borrowed, so the line is longer than what is up there.
|
|
MVQA
|
|
BRI editRedrawPad
|
|
editRedrawGrew:
|
|
RSTA
|
|
editRedrawPad:
|
|
SETD.1 EditPrinted
|
|
STA.1
|
|
CALL printSpaces
|
|
|
|
; How much went to the screen altogether, which is how far back the start of the line is.
|
|
SETD.1 EditPrinted
|
|
LDA.1
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
SETD.1 EditPrinted
|
|
STA.1
|
|
|
|
; And what is up there now is exactly the line.
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditDrawn
|
|
STA.1
|
|
|
|
CALL editAnchor
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
CALL editPlace
|
|
RET
|
|
|
|
; ---- Where the line starts, worked out backwards ----
|
|
;
|
|
; The console has just been told to print, so it knows where printing ENDED, and the line
|
|
; began that many characters before it. Asking afterwards rather than trusting what was
|
|
; remembered is what makes this survive the screen SCROLLING: a line printed on the bottom
|
|
; row moves everything up by one, and a remembered row would be one too low from then on.
|
|
editAnchor:
|
|
INA 0x03
|
|
SETD.1 EditWalkRow
|
|
STA.1
|
|
INA 0x04
|
|
SETD.1 EditWalkColumn
|
|
STA.1
|
|
SETD.1 EditPrinted
|
|
LDA.1
|
|
SETD.1 EditWalkBack
|
|
STA.1
|
|
editAnchorWalk:
|
|
SETD.1 EditWalkColumn
|
|
LDA.1
|
|
SETD.1 EditWalkBack
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRC editAnchorRowUp ; Further back than this row goes.
|
|
MVQA
|
|
SETD.1 EditWalkColumn
|
|
STA.1
|
|
BRI editAnchorDone
|
|
editAnchorRowUp:
|
|
; Off the front of this row, so take what the row used and carry on along the one above.
|
|
SETD.1 EditWalkBack
|
|
LDA.1
|
|
SETD.1 EditWalkColumn
|
|
LDB.1
|
|
INCB ; The column itself, and the step onto it.
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
SETD.1 EditWalkBack
|
|
STA.1
|
|
SETD.1 EditWidth
|
|
LDA.1
|
|
DECA
|
|
SETD.1 EditWalkColumn
|
|
STA.1
|
|
SETD.1 EditWalkRow
|
|
LDA.1
|
|
BRA editAnchorTop
|
|
DECA
|
|
STA.1
|
|
BRI editAnchorWalk
|
|
editAnchorTop:
|
|
; The line began off the top of the screen, which means more was scrolled away than the
|
|
; line is long. There is nothing sensible left to point at, so it starts in the corner.
|
|
RSTA
|
|
SETD.1 EditWalkColumn
|
|
STA.1
|
|
editAnchorDone:
|
|
SETD.1 EditWalkRow
|
|
LDA.1
|
|
SETD.1 EditRow
|
|
STA.1
|
|
SETD.1 EditWalkColumn
|
|
LDA.1
|
|
SETD.1 EditColumn
|
|
STA.1
|
|
RET
|
|
|
|
; A says how many characters along the line the cursor belongs. Puts it there, carrying onto
|
|
; the rows below when the line is longer than one row of the screen.
|
|
editPlace:
|
|
SETD.1 EditColumn
|
|
LDB.1
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
SETD.1 EditWalkColumn
|
|
STA.1
|
|
SETD.1 EditRow
|
|
LDA.1
|
|
SETD.1 EditWalkRow
|
|
STA.1
|
|
editPlaceWrap:
|
|
SETD.1 EditWalkColumn
|
|
LDA.1
|
|
SETD.1 EditWidth
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRC editPlaceDone ; Inside the row.
|
|
MVQA
|
|
SETD.1 EditWalkColumn
|
|
STA.1
|
|
SETD.1 EditWalkRow
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI editPlaceWrap
|
|
editPlaceDone:
|
|
SETD.1 EditWalkRow
|
|
LDA.1
|
|
OUTA 0x03
|
|
SETD.1 EditWalkColumn
|
|
LDA.1
|
|
OUTA 0x04
|
|
RET
|
|
|
|
; ---- What was typed before ----
|
|
;
|
|
; Eight lines, oldest first, in a RING: a ninth pushes the oldest out by moving where the
|
|
; ring starts rather than by moving any of the lines. Eight is a power of two, so which slot
|
|
; an entry lives in is an AND rather than a division, and nothing is ever copied to make
|
|
; room.
|
|
;
|
|
; A NINTH SLOT HOLDS WHAT WAS BEING TYPED when Up was first pressed, and Down walks back into
|
|
; it. Losing a half written line to a keypress is exactly the sort of small rudeness that
|
|
; makes a thing unpleasant to use, and it costs one slot to avoid.
|
|
;
|
|
; The history belongs to the SHELL rather than to the console, which is the whole reason it
|
|
; can exist at all: until the keys arrived here, there was nothing to press Up at.
|
|
|
|
; DP3 to history entry A, where entry nought is the oldest one still kept.
|
|
historySlot:
|
|
SETD.1 HistoryStart
|
|
LDB.1
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
INIB 0d7
|
|
AND
|
|
MVQA
|
|
SETD.3 HistoryLines
|
|
|
|
; A slot is 128 bytes, and A and B are a sixteen bit shift register: one rotation right
|
|
; with B empty turns the slot number into the offset of the slot, high byte and low.
|
|
RSTB
|
|
SHR
|
|
DPUW.3
|
|
RET
|
|
|
|
; Copies the line being edited into the slot DP3 names.
|
|
historyPut:
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
SETD.1 EditLength
|
|
LDB.1
|
|
historyPutNext:
|
|
BRB historyPutEnd
|
|
LDA.0
|
|
STA.3
|
|
INCD.0
|
|
INCD.3
|
|
DECB
|
|
BRI historyPutNext
|
|
historyPutEnd:
|
|
RSTA
|
|
STA.3
|
|
RET
|
|
|
|
; Copies the line in the slot DP3 names into the buffer, and puts the cursor at the end of
|
|
; it, which is where somebody who has just recalled a line wants to be.
|
|
;
|
|
; A LINE TOO LONG FOR THE BUFFER IS CUT SHORT rather than written past the end of it. The
|
|
; history is shared with the monitor, which reads into forty bytes where the shell reads
|
|
; into a hundred and twenty seven, so this is a real case and not a defensive one.
|
|
historyTake:
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
RSTA
|
|
SETD.1 EditLength
|
|
STA.1
|
|
historyTakeNext:
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditRoom
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNC historyTakeDone ; No room for another character.
|
|
LDA.3
|
|
BRA historyTakeDone
|
|
STA.0
|
|
INCD.0
|
|
INCD.3
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI historyTakeNext
|
|
historyTakeDone:
|
|
RSTA
|
|
STA.0
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
SETD.1 EditAt
|
|
STA.1
|
|
RET
|
|
|
|
; DP0 and DP3 name strings ending in zero bytes. Q is zero if they are the same.
|
|
;
|
|
; textSame does this already and takes its two strings in DP0 and DP1, and getting a pointer
|
|
; from DP3 into DP1 costs a store, a scratch word and a load. This is shorter than that and
|
|
; says what it is doing.
|
|
historySameAs:
|
|
LDA.0
|
|
LDB.3
|
|
XOR
|
|
BNQ historySameNo
|
|
LDA.0
|
|
BRA historySameYes ; They matched and they both ended.
|
|
INCD.0
|
|
INCD.3
|
|
BRI historySameAs
|
|
historySameNo:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is one.
|
|
RET
|
|
historySameYes:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero.
|
|
RET
|
|
|
|
; The line just finished, kept. An empty one is not - that is somebody pressing Return - and
|
|
; neither is one the same as the newest already there, because running a command twice
|
|
; should not put it in twice.
|
|
historyAdd:
|
|
SETD.1 EditLength
|
|
LDA.1
|
|
BRA historyAddDone
|
|
|
|
SETD.1 HistoryCount
|
|
LDA.1
|
|
BRA historyAddPut ; Nothing kept yet, so nothing to be the same as.
|
|
DECA
|
|
CALL historySlot
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
CALL historySameAs
|
|
BRQ historyAddDone
|
|
|
|
historyAddPut:
|
|
SETD.1 HistoryCount
|
|
LDA.1
|
|
INIB 0d8
|
|
CCF
|
|
SUB
|
|
BRC historyAddRoom ; Fewer than eight, so there is a slot on the end.
|
|
|
|
; Full. The new line goes over the oldest and the ring starts one further along, which
|
|
; forgets the oldest without moving any of the others.
|
|
INIA 0d8
|
|
CALL historySlot
|
|
CALL historyPut
|
|
SETD.1 HistoryStart
|
|
LDA.1
|
|
INCA
|
|
INIB 0d7
|
|
AND
|
|
MVQA
|
|
STA.1
|
|
BRI historyAddDone
|
|
|
|
historyAddRoom:
|
|
SETD.1 HistoryCount
|
|
LDA.1
|
|
CALL historySlot
|
|
CALL historyPut
|
|
SETD.1 HistoryCount
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
|
|
historyAddDone:
|
|
; However that went, the next Up starts from the newest again.
|
|
SETD.1 HistoryCount
|
|
LDA.1
|
|
SETD.1 HistoryPick
|
|
STA.1
|
|
RET
|
|
|
|
; ---- When something goes wrong that nothing can carry on past ----
|
|
;
|
|
; A fault used to stop the machine and print a line to whatever was behind it. On a terminal
|
|
; that is a diagnosis; behind a window it is a frozen picture and no reason at all, because
|
|
; the message goes to a standard error nobody is looking at. The machine looked hung and was
|
|
; not - it had stopped, and said so somewhere invisible.
|
|
;
|
|
; So the system catches all five and says it on the screen instead.
|
|
;
|
|
; ---- A fault ends the PROGRAM, not the machine ----
|
|
;
|
|
; That is the answer to "carry on or start again", and it is not a compromise: a bare RETI
|
|
; from most of these meets the very instruction that failed and fails again, so carrying on
|
|
; is not on offer. But the machine is almost never what is broken. Everything the shell puts
|
|
; back when a program exits - the Stack, the vectors it installed, the drive, the working
|
|
; directory, the console, the screen - is exactly what wants putting back after one dies, so
|
|
; a fault in a loaded program joins handleExit and you are back at the prompt.
|
|
;
|
|
; A fault BELOW where programs load is the system's own, and there is nothing to go back to.
|
|
; That one says so and stops.
|
|
;
|
|
; ---- What a handler must not do ----
|
|
;
|
|
; Fault. There is no double fault rule on this machine: a handler that commits the fault it
|
|
; was called about is called again, forever, and each time costs another frame of Stack. So
|
|
; nothing below asks for a service, touches a disk, or reaches anything that can refuse.
|
|
faultBadOpcode:
|
|
SETD.0 FaultOpcode
|
|
BRI faultPlain
|
|
faultGuard:
|
|
SETD.0 FaultGuard
|
|
BRI faultPlain
|
|
faultBank:
|
|
SETD.0 FaultBank
|
|
faultPlain:
|
|
; The Stack Pointer names the frame, and it has to be taken before anything pushes. Nothing
|
|
; above here does: a SETD and a branch move no Stack.
|
|
MVSD.3
|
|
RSTA
|
|
SETD.1 FaultNumbered
|
|
STA.1
|
|
BRI faultSay
|
|
|
|
faultNoHandler:
|
|
SETD.0 FaultNoHandler
|
|
BRI faultNumbered
|
|
faultNoDevice:
|
|
SETD.0 FaultNoDevice
|
|
faultNumbered:
|
|
MVSD.3
|
|
; These two are the only faults that say WHICH one, and the machine hands that over in Q -
|
|
; the one thing a handler here is given in a register. Kept at once, before printing can
|
|
; disturb it.
|
|
MVQA
|
|
SETD.1 FaultNumber
|
|
STA.1
|
|
INIA 0x01
|
|
SETD.1 FaultNumbered
|
|
STA.1
|
|
|
|
faultSay:
|
|
; The screen before the words. There is no use saying any of this somewhere it cannot be
|
|
; read, and a program that faulted may have left the screen with nowhere to put a letter.
|
|
CALL faultScreen
|
|
|
|
CALL printString
|
|
SETD.1 FaultNumbered
|
|
LDA.1
|
|
BRA faultWhere
|
|
SETD.1 FaultNumber
|
|
LDA.1
|
|
CALL printByteHex
|
|
faultWhere:
|
|
SETD.0 FaultAt
|
|
CALL printString
|
|
CALL faultAddress
|
|
CALL newLine
|
|
|
|
; The registers as the frame kept them, which is what they were when it happened.
|
|
SETD.0 FaultRegisters
|
|
CALL printString
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d3
|
|
LDA.0
|
|
CALL printByteHex
|
|
SETD.0 FaultB
|
|
CALL printString
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d4
|
|
LDA.0
|
|
CALL printByteHex
|
|
SETD.0 FaultQ
|
|
CALL printString
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d2
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
|
|
; ---- Whose fault it was ----
|
|
;
|
|
; Where it happened says which, and one byte of the address decides it: a loaded program
|
|
; begins at 0x4000 and everything below that is the system.
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d13
|
|
LDA.0
|
|
INIB 0x50
|
|
CCF
|
|
SUB
|
|
BRC faultInSystem
|
|
|
|
; A program that will not be carrying on. handleExit does all of the putting back, and
|
|
; takes the status the program is deemed to have stopped with in A.
|
|
INIA 0x01
|
|
SETD.1 FaultStopped
|
|
STA.1
|
|
INIA 0xFF
|
|
BRI handleExit
|
|
|
|
faultInSystem:
|
|
; Nothing to go back to: the shell IS what faulted, and its Stack, its variables and its
|
|
; place in its own code are all suspect. Saying so and stopping is the only honest answer,
|
|
; and it is a great deal better than the frozen picture this used to be.
|
|
SETD.0 FaultSystem
|
|
CALL printString
|
|
CALL newLine
|
|
HALT
|
|
|
|
; The two bytes of the address in the frame, printed high half first.
|
|
faultAddress:
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d13
|
|
LDA.0
|
|
CALL printByteHex
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
RET
|
|
|
|
; ---- A screen this can be read on ----
|
|
;
|
|
; "Put the screen in a known mode" is not tidiness. A program that left the screen in bitmap
|
|
; mode left nowhere to draw a character at all - the console draws nothing when there are no
|
|
; text rows - so without this, the message about what went wrong is invisible, which is the
|
|
; one thing it must never be.
|
|
faultScreen:
|
|
INIA 0x01
|
|
OUTA 0x31 ; Eighty columns of text, whatever was being used.
|
|
|
|
; And the whole view back to the corner. A scrolled origin or a fraction of a cell puts
|
|
; every character somewhere other than where it says it is.
|
|
RSTA
|
|
OUTA 0x34
|
|
OUTA 0x36
|
|
OUTA 0x37
|
|
OUTA 0x38
|
|
|
|
; ---- Colours it can be read in ----
|
|
;
|
|
; A known mode is only half of a known screen: a program that wrote its own palette may
|
|
; have left every ink the same as every paper. Attribute one draws in palette entries 16
|
|
; and 17, so those two are written and the rest of the program's colours are left alone -
|
|
; there is nothing to be gained here by taking away more than is needed.
|
|
; The glyphs first, since a program is as free to redefine a letter as any other tile and
|
|
; a message spelled in somebody's tile graphics is no message at all. It touches only the
|
|
; console's own tiles, so a program's are left where they are.
|
|
INIA 0x01
|
|
OUTA 0x39
|
|
|
|
CALL screenBank
|
|
INIA 0d4
|
|
OUTA 0xE3
|
|
INIA 0xFC
|
|
OUTA 0xE4
|
|
INIA 0x40
|
|
OUTA 0xE5 ; 0xFC40, which is entry sixteen.
|
|
|
|
RSTA
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
OUTA 0xE9 ; Black paper.
|
|
INIA 0xD0
|
|
OUTA 0xE9
|
|
INIA 0x40
|
|
OUTA 0xE9
|
|
INIA 0x38
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9 ; Red ink, the same red the machine wakes up with.
|
|
|
|
INIA 0x01
|
|
OUTA 0x06 ; And draw in it.
|
|
|
|
; A console that can be printed to at all: line mode, a cursor, nothing interrupting.
|
|
INIA 0x04
|
|
OUTA 0x02
|
|
RET
|
|
|
|
; ---- Finishing a word somebody started ----
|
|
;
|
|
; Tab. What it can finish depends on where in the line it is: the FIRST word is a command or
|
|
; the name of a program, and everything after it is a file. Only the first is done here.
|
|
;
|
|
; None of this could have existed a fortnight ago. The shell never saw a keystroke - the
|
|
; terminal handed it a finished line - so there was no moment at which somebody had typed
|
|
; half a word and the system knew about it. That is the same wall the history was behind.
|
|
;
|
|
; ---- What it does with what it finds ----
|
|
;
|
|
; One match goes in with a space after it, because a word that can only be one thing is
|
|
; finished. Several are folded into their longest common prefix and that goes in, which is
|
|
; the most that can be said without guessing. If that adds nothing - because what is typed
|
|
; IS already the common prefix - the matches are listed and the line put back underneath.
|
|
;
|
|
; EVERY HELPER BELOW ANSWERS IN Q. A RET puts A, B and the first three pointers back, so a
|
|
; routine here that answered in A would be answering into a register its caller cannot see.
|
|
tabComplete:
|
|
; ---- Which word, and is it the first ----
|
|
;
|
|
; Back from the insertion point to a space or the start of the line. What lies between
|
|
; there and the cursor is what somebody has typed, and what lies before it decides whether
|
|
; this is a command being named or an argument being given.
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
SETD.1 TabWordAt
|
|
STA.1
|
|
tabBack:
|
|
SETD.1 TabWordAt
|
|
LDA.1
|
|
BRA tabAtLineStart ; The start of the line is the start of the word, and of the line.
|
|
DECA
|
|
CALL tabCharAt
|
|
MVQA
|
|
INIB 0x20
|
|
XOR
|
|
BRQ tabFoundStart ; A space, so the word begins after it.
|
|
SETD.1 TabWordAt
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BRI tabBack
|
|
|
|
tabAtLineStart:
|
|
INIA 0x01
|
|
SETD.1 TabFirst
|
|
STA.1
|
|
BRI tabIsFirst
|
|
|
|
tabFoundStart:
|
|
; There is a space behind the word, so something may be in front of that space. Only a run
|
|
; of spaces means this is still the first word - and which word it is decides what the word
|
|
; could BE: a command or a program at the front of a line, a file anywhere after it.
|
|
INIA 0x01
|
|
SETD.1 TabFirst
|
|
STA.1
|
|
SETD.1 TabWordAt
|
|
LDA.1
|
|
SETD.1 TabLeft
|
|
STA.1
|
|
tabFirstCheck:
|
|
SETD.1 TabLeft
|
|
LDA.1
|
|
BRA tabIsFirst
|
|
DECA
|
|
STA.1
|
|
CALL tabCharAt
|
|
MVQA
|
|
INIB 0x20
|
|
XOR
|
|
BRQ tabFirstCheck
|
|
RSTA
|
|
SETD.1 TabFirst
|
|
STA.1 ; A word came before this one, so this is an argument.
|
|
|
|
tabIsFirst:
|
|
; How much of it is typed. Nothing at all is not a question anybody asked - it would offer
|
|
; every command there is, which is what help is for.
|
|
SETD.1 EditAt
|
|
LDA.1
|
|
SETD.1 TabWordAt
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ tabDone
|
|
MVQA
|
|
SETD.1 TabWordLen
|
|
STA.1
|
|
|
|
RSTA
|
|
SETD.1 TabCount
|
|
STA.1
|
|
SETD.1 TabBestLen
|
|
STA.1
|
|
|
|
; A path in the word says which directory to look in, and narrows what is being matched to
|
|
; the part after the last separator.
|
|
CALL tabSplit
|
|
|
|
; ---- What this word could be ----
|
|
;
|
|
; The shell's own commands, only at the front of a line and only when no directory has been
|
|
; named: nothing with a separator in it was ever going to be "dir".
|
|
;
|
|
; And on the disk: programs at the front of a line, since that is all a first word can
|
|
; usefully be, and anything at all after it.
|
|
SETD.1 TabFirst
|
|
LDA.1
|
|
SETD.1 TabPrograms
|
|
STA.1
|
|
CALL tabRunSources
|
|
|
|
SETD.1 TabCount
|
|
LDA.1
|
|
BRA tabDone ; Nothing matched, and saying so would be noise.
|
|
|
|
; ---- Putting it in ----
|
|
;
|
|
; Everything of the answer past what was already typed. The insertion point is at the end
|
|
; of the word, so each character goes in where the one before it left off.
|
|
SETD.1 TabWordLen
|
|
LDA.1
|
|
SETD.1 TabPut
|
|
STA.1
|
|
tabPutNext:
|
|
SETD.1 TabPut
|
|
LDA.1
|
|
SETD.1 TabBestLen
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
; At or past, rather than exactly equal. They cannot pass each other now - a candidate is
|
|
; only offered if it matches what was typed, so the answer is never shorter than that - but
|
|
; they DID while the answer was being taken from the wrong place, and the difference
|
|
; between the two branches was a line that ran off the end and a line that simply did not
|
|
; grow. The cheaper failure is worth a branch that costs nothing.
|
|
BNC tabPutDone
|
|
SETD.3 TabBest
|
|
SETD.1 TabPut
|
|
LDA.1
|
|
DPUA.3
|
|
LDA.3
|
|
SETD.1 EditChar
|
|
STA.1
|
|
CALL editPut
|
|
BNQ tabPutDone ; The line is full, so the rest of the answer will not fit.
|
|
SETD.1 TabPut
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI tabPutNext
|
|
tabPutDone:
|
|
|
|
SETD.1 TabCount
|
|
LDA.1
|
|
INIB 0d1
|
|
XOR
|
|
BNQ tabAmbiguous
|
|
|
|
; Only one thing it can be, so it is finished and a space says so - unless it is a
|
|
; directory, which answered with a separator on the end and is somewhere to carry on typing
|
|
; rather than something to have finished.
|
|
SETD.3 TabBest
|
|
SETD.1 TabBestLen
|
|
LDA.1
|
|
DECA
|
|
DPUA.3
|
|
LDA.3
|
|
INIB 0x2F
|
|
XOR
|
|
BRQ tabSaidDirectory
|
|
|
|
INIA 0x20
|
|
SETD.1 EditChar
|
|
STA.1
|
|
CALL editPut
|
|
tabSaidDirectory:
|
|
CALL editRedraw
|
|
BRI tabDone
|
|
|
|
tabAmbiguous:
|
|
; Several. The common prefix has gone in; if it added nothing then what was typed is
|
|
; already as far as they all agree, and the only useful thing left is to show them.
|
|
SETD.1 TabBestLen
|
|
LDA.1
|
|
SETD.1 TabWordLen
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ tabList
|
|
CALL editRedraw
|
|
tabDone:
|
|
BRI editKey
|
|
|
|
; Everywhere a candidate can come from, in the order they are tried. Run once to find the
|
|
; answer and again to show the matches, which is why it is a routine and not a run of code.
|
|
tabRunSources:
|
|
SETD.1 TabFirst
|
|
LDA.1
|
|
BRA tabNoNames
|
|
SETD.1 TabDirLen
|
|
LDA.1
|
|
BNA tabNoNames
|
|
CALL tabTryNames
|
|
tabNoNames:
|
|
CALL tabTryFiles
|
|
RET
|
|
|
|
; A holds an offset into the line. Q is the character there.
|
|
tabCharAt:
|
|
SETD.1 EditBase
|
|
LDD.0.1
|
|
DPUA.0
|
|
LDA.0
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Offering a candidate ----
|
|
;
|
|
; DP3 names a string ending in a zero. If it begins with what has been typed it is folded
|
|
; into the answer, and DP3 is left just past that zero either way - which is what makes a run
|
|
; of packed strings walkable by calling this over and over.
|
|
tabOffer:
|
|
; Where this candidate begins, before the comparison below walks DP3 through it. What is
|
|
; taken as the answer is the WHOLE name and not the part after the bit that matched.
|
|
SETD.1 TabStart
|
|
STD.3.1
|
|
SETD.1 TabWordLen
|
|
LDA.1
|
|
SETD.1 TabSeen
|
|
STA.1
|
|
RSTA
|
|
SETD.1 TabAt
|
|
STA.1
|
|
tabOfferMatch:
|
|
SETD.1 TabSeen
|
|
LDA.1
|
|
BRA tabOfferTake ; Every typed character agreed.
|
|
DECA
|
|
STA.1
|
|
|
|
LDA.3
|
|
BRA tabOfferSkip ; The candidate ended before what was typed did.
|
|
SETD.1 TabHold
|
|
STA.1 ; Kept, while the line's character is fetched.
|
|
|
|
SETD.1 TabWordAt
|
|
LDA.1
|
|
SETD.1 TabAt
|
|
LDB.1
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
CALL tabCharAt
|
|
MVQA
|
|
SETD.1 TabHold
|
|
LDB.1
|
|
XOR
|
|
BNQ tabOfferSkip
|
|
|
|
INCD.3
|
|
SETD.1 TabAt
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI tabOfferMatch
|
|
|
|
tabOfferTake:
|
|
; ---- Folded in, or shown ----
|
|
;
|
|
; The same walk answers both questions, because they are the same question asked twice:
|
|
; what matches. Listing used to be a second copy of the walk that knew only about the
|
|
; shell's own words, so the files it should have been showing were never there.
|
|
SETD.1 TabShowing
|
|
LDA.1
|
|
BNA tabOfferPrint
|
|
|
|
; The first match is the answer. Every one after it cuts the answer back to where the two
|
|
; stop agreeing, which is all that can be said without guessing which was meant.
|
|
SETD.1 TabCount
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
INIB 0d1
|
|
XOR
|
|
BRQ tabOfferFirst
|
|
CALL tabNarrow
|
|
BRI tabOfferSkip
|
|
tabOfferFirst:
|
|
CALL tabTake
|
|
BRI tabOfferSkip
|
|
|
|
tabOfferPrint:
|
|
SETD.1 TabStart
|
|
LDD.0.1
|
|
CALL printString
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
OUTA 0x00 ; Two spaces between them, and A already holds one.
|
|
|
|
tabOfferSkip:
|
|
; Past the end of this candidate, from wherever the comparison stopped.
|
|
LDA.3
|
|
BRA tabOfferPast
|
|
INCD.3
|
|
BRI tabOfferSkip
|
|
tabOfferPast:
|
|
INCD.3
|
|
RET
|
|
|
|
; The candidate DP3 names becomes the answer. DP3 is put somewhere and read back rather than
|
|
; walked, because it is the only pointer that survives a call and the caller still needs it.
|
|
tabTake:
|
|
SETD.1 TabStart
|
|
LDD.0.1
|
|
SETD.2 TabBest
|
|
RSTA
|
|
SETD.1 TabBestLen
|
|
STA.1
|
|
tabTakeNext:
|
|
SETD.1 TabBestLen
|
|
LDA.1
|
|
INIB 0d63
|
|
CCF
|
|
SUB
|
|
BNC tabTakeDone ; As much of a name as there is room for.
|
|
LDA.0
|
|
BRA tabTakeDone
|
|
STA.2
|
|
INCD.0
|
|
INCD.2
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI tabTakeNext
|
|
tabTakeDone:
|
|
RET
|
|
|
|
; The answer is cut back to where it and the candidate DP3 names stop agreeing.
|
|
tabNarrow:
|
|
SETD.1 TabStart
|
|
LDD.0.1
|
|
SETD.2 TabBest
|
|
RSTA
|
|
SETD.1 TabNarrowAt
|
|
STA.1
|
|
tabNarrowNext:
|
|
SETD.1 TabNarrowAt
|
|
LDA.1
|
|
SETD.1 TabBestLen
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNC tabNarrowDone ; The answer ran out first, so all of it still agrees.
|
|
LDA.0
|
|
BRA tabNarrowCut ; The candidate ran out, so the answer stops here.
|
|
LDB.2
|
|
XOR
|
|
BNQ tabNarrowCut
|
|
INCD.0
|
|
INCD.2
|
|
SETD.1 TabNarrowAt
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI tabNarrowNext
|
|
tabNarrowCut:
|
|
SETD.1 TabNarrowAt
|
|
LDA.1
|
|
SETD.1 TabBestLen
|
|
STA.1
|
|
tabNarrowDone:
|
|
RET
|
|
|
|
|
|
; ---- Splitting the word at its last separator ----
|
|
;
|
|
; "Apps/Sn" is a directory to look in and a name to match, and everything downstream depends
|
|
; on getting that split right. What comes back is TabDir holding the directory part - empty
|
|
; when there is no separator, meaning where the machine already is - and TabWordAt and
|
|
; TabWordLen narrowed to the name part, so that matching and inserting go on working on the
|
|
; part of the word they were always about.
|
|
tabSplit:
|
|
RSTA
|
|
SETD.1 TabDirLen
|
|
STA.1
|
|
|
|
; Backwards, so the LAST separator is the one found.
|
|
SETD.1 TabWordLen
|
|
LDA.1
|
|
SETD.1 TabAt
|
|
STA.1
|
|
tabSplitBack:
|
|
SETD.1 TabAt
|
|
LDA.1
|
|
BRA tabSplitNone ; No separator anywhere in it.
|
|
DECA
|
|
STA.1
|
|
SETD.1 TabWordAt
|
|
LDB.1
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
CALL tabCharAt
|
|
MVQA
|
|
INIB 0x2F
|
|
XOR
|
|
BNQ tabSplitBack
|
|
|
|
; Found one. The directory part is everything up to and including it, which is what makes
|
|
; "/Source/" and "Source/" both say what they mean.
|
|
SETD.1 TabAt
|
|
LDA.1
|
|
INCA
|
|
SETD.1 TabDirLen
|
|
STA.1
|
|
|
|
SETD.2 TabDir
|
|
RSTA
|
|
SETD.1 TabCopyAt
|
|
STA.1
|
|
tabSplitCopy:
|
|
SETD.1 TabCopyAt
|
|
LDA.1
|
|
SETD.1 TabDirLen
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNC tabSplitCopied
|
|
SETD.1 TabWordAt
|
|
LDA.1
|
|
SETD.1 TabCopyAt
|
|
LDB.1
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
CALL tabCharAt
|
|
MVQA
|
|
STA.2
|
|
INCD.2
|
|
SETD.1 TabCopyAt
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI tabSplitCopy
|
|
tabSplitCopied:
|
|
RSTA
|
|
STA.2 ; The zero that ends it.
|
|
|
|
; And the word being matched is what comes after the separator.
|
|
SETD.1 TabWordAt
|
|
LDA.1
|
|
SETD.1 TabDirLen
|
|
LDB.1
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
SETD.1 TabWordAt
|
|
STA.1
|
|
SETD.1 TabWordLen
|
|
LDA.1
|
|
SETD.1 TabDirLen
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
SETD.1 TabWordLen
|
|
STA.1
|
|
RET
|
|
tabSplitNone:
|
|
SETD.0 TabDir
|
|
RSTA
|
|
STA.0
|
|
RET
|
|
|
|
; ---- What is on the disk ----
|
|
;
|
|
; Every entry of the directory the word names that begins with the rest of the word. A
|
|
; directory is offered with a separator after it, which says what it is and lets the next
|
|
; part be typed straight away - and, because the answer then ends in one, is also what stops
|
|
; a space being put after it.
|
|
;
|
|
; WHERE THE MACHINE IS COMES BACK. This walks somebody else's directory by standing in it,
|
|
; which is the only way to walk one here, so where they were and which drive they were on are
|
|
; put down first and restored whatever happens.
|
|
tabTryFiles:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA tabFilesNone
|
|
|
|
INA 0x24
|
|
SETD.1 TabWasDrive
|
|
STA.1
|
|
SETD.0 SbfsCwd
|
|
SETD.1 TabWasCwd
|
|
CALL sbfsCopyWord
|
|
|
|
CALL tabWalkNamed
|
|
|
|
; ---- And the places a program is looked for ----
|
|
;
|
|
; The first word of a line is a command or a program, and the shell looks for a program in
|
|
; three places: where you are, the system's own place for them on the disk you are on, and
|
|
; that same place on drive 0. Tab offers what the shell would find, or it would finish a
|
|
; word into something that then would not run.
|
|
;
|
|
; ONLY WHEN NO DIRECTORY WAS NAMED. "Apps/Sn" means that directory and nowhere else.
|
|
SETD.1 TabFirst
|
|
LDA.1
|
|
BRA tabFilesBack
|
|
SETD.1 TabDirLen
|
|
LDA.1
|
|
BNA tabFilesBack
|
|
|
|
CALL tabWalkApps
|
|
|
|
; And drive 0's, unless that is the drive we are already on - in which case it is the same
|
|
; directory, and offering everything in it twice would mean nothing was ever the only
|
|
; match.
|
|
SETD.1 TabWasDrive
|
|
LDA.1
|
|
BRA tabFilesBack
|
|
RSTA
|
|
CALL sbfsUse
|
|
CALL tabWalkApps
|
|
BRI tabFilesBack
|
|
|
|
; ---- Everything in a directory, offered ----
|
|
;
|
|
; Two ways in, because there are two kinds of place to look: the one the word named, and the
|
|
; system's own place for programs. THE FIRST MUST NOT BE OVERWRITTEN BY THE SECOND - an
|
|
; earlier version wrote "/Apps" into TabDir, and since the whole walk runs a second time to
|
|
; list the matches, that second run looked in /Apps for a word which had named nowhere, and
|
|
; showed nothing at all.
|
|
;
|
|
; Where the machine stands is moved and put back by the caller, which is what makes calling
|
|
; either of these more than once safe.
|
|
tabWalkApps:
|
|
SETD.0 TabWasCwd
|
|
SETD.1 SbfsCwd
|
|
CALL sbfsCopyWord
|
|
SETD.0 AppsPath
|
|
BRI tabWalkTo
|
|
|
|
tabWalkNamed:
|
|
SETD.0 TabWasCwd
|
|
SETD.1 SbfsCwd
|
|
CALL sbfsCopyWord
|
|
|
|
SETD.1 TabDirLen
|
|
LDA.1
|
|
BRA tabFilesHere ; No directory named, so it is this one.
|
|
|
|
SETD.0 TabDir
|
|
tabWalkTo:
|
|
CALL sbfsWalk
|
|
BNQ tabWalkNothing ; No such path, and nothing to offer out of it.
|
|
|
|
; The root is not an entry, so it has no flags to ask - and it is the one place that can be
|
|
; walked to without there being anything to have asked.
|
|
SETD.0 SbfsAt
|
|
LDA.0
|
|
INCD.0
|
|
LDB.0
|
|
OR
|
|
BRQ tabFilesGo
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x02
|
|
AND
|
|
BRQ tabWalkNothing ; Not a directory, so there is nothing in it.
|
|
tabFilesGo:
|
|
SETD.0 SbfsAt
|
|
SETD.1 SbfsCwd
|
|
CALL sbfsCopyWord
|
|
|
|
tabFilesHere:
|
|
CALL sbfsFirst
|
|
BRI tabFilesCheck
|
|
tabFilesStep:
|
|
CALL sbfsNext
|
|
tabFilesCheck:
|
|
BNQ tabWalkNothing
|
|
CALL tabEntry
|
|
BRI tabFilesStep
|
|
tabWalkNothing:
|
|
RET
|
|
|
|
tabFilesBack:
|
|
SETD.0 TabWasCwd
|
|
SETD.1 SbfsCwd
|
|
CALL sbfsCopyWord
|
|
SETD.1 TabWasDrive
|
|
LDA.1
|
|
CALL sbfsUse
|
|
tabFilesNone:
|
|
RET
|
|
|
|
; The entry the walk is standing on, offered under the name somebody would type for it.
|
|
tabEntry:
|
|
SETD.0 SbfsName
|
|
SETD.1 TabCandidate
|
|
CALL tabCopyName
|
|
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x02
|
|
AND
|
|
BRQ tabEntryFile ; The bit SET is what says directory - see dir, which asks the
|
|
; same question the same way round.
|
|
|
|
; A directory, and the separator goes on the end of it.
|
|
SETD.3 TabCandidate
|
|
CALL tabEnd
|
|
INIA 0x2F
|
|
STA.3
|
|
INCD.3
|
|
RSTA
|
|
STA.3
|
|
BRI tabEntryOffer
|
|
|
|
tabEntryFile:
|
|
; A file. When the first word of a line is being finished it can only usefully be a
|
|
; program, and the name to type for one is the name without its extension - so anything
|
|
; else is not offered at all rather than offered and useless.
|
|
SETD.1 TabPrograms
|
|
LDA.1
|
|
BRA tabEntryOffer
|
|
CALL tabStripSbx
|
|
BNQ tabEntryDone
|
|
|
|
tabEntryOffer:
|
|
SETD.3 TabCandidate
|
|
CALL tabOffer
|
|
tabEntryDone:
|
|
RET
|
|
|
|
; DP0 names a string and DP1 where to put it. Copies up to what a name can be.
|
|
tabCopyName:
|
|
RSTB
|
|
tabCopyNext:
|
|
LDA.0
|
|
BRA tabCopyDone
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
INCB
|
|
INIA 0d40
|
|
CCF
|
|
SUB
|
|
BRQ tabCopyDone
|
|
BRI tabCopyNext
|
|
tabCopyDone:
|
|
RSTA
|
|
STA.1
|
|
RET
|
|
|
|
; DP3 names a string. Leaves DP3 on its zero.
|
|
tabEnd:
|
|
LDA.3
|
|
BRA tabEndDone
|
|
INCD.3
|
|
BRI tabEnd
|
|
tabEndDone:
|
|
RET
|
|
|
|
; Takes ".sbx" off the end of TabCandidate. Q is zero if it was there, and one if it was not
|
|
; - which is how a file that is not a program is passed over.
|
|
tabStripSbx:
|
|
; How long the name is, counted rather than measured from a pointer, because what is
|
|
; wanted next is a NUMBER: four characters back from the end is only a place if there are
|
|
; four of them to go back over.
|
|
SETD.0 TabCandidate
|
|
RSTB
|
|
tabStripCount:
|
|
LDA.0
|
|
BRA tabStripCounted
|
|
INCD.0
|
|
INCB
|
|
BRI tabStripCount
|
|
tabStripCounted:
|
|
; The count is in B and wanted in A, and there is no instruction that moves one to the
|
|
; other: adding it to nothing is how a value crosses from B to A on this machine.
|
|
RSTA
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
INIB 0d4
|
|
CCF
|
|
SUB
|
|
BRC tabStripNo ; Shorter than the extension, so it cannot end in it.
|
|
MVQA
|
|
SETD.3 TabCandidate
|
|
DPUA.3 ; Four from the end, which is where the extension would begin.
|
|
|
|
SETD.0 SbxText
|
|
tabStripMatch:
|
|
LDA.0
|
|
BRA tabStripYes
|
|
LDB.3
|
|
XOR
|
|
BNQ tabStripNo
|
|
INCD.0
|
|
INCD.3
|
|
BRI tabStripMatch
|
|
tabStripYes:
|
|
; It ended in the extension, so the name to type is what comes before it. DP3 was left on
|
|
; the zero by the comparison, so four back is where the dot was.
|
|
INIA 0d4
|
|
DPDA.3
|
|
RSTA
|
|
STA.3
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: it is a program.
|
|
RET
|
|
tabStripNo:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is one: it is not.
|
|
RET
|
|
|
|
; ---- The shell's own words ----
|
|
;
|
|
; Fifteen strings packed end to end, each ending in the zero that says where the next one
|
|
; begins. That is why they are laid out that way: a chain of comparisons can be executed and
|
|
; cannot be walked.
|
|
tabTryNames:
|
|
SETD.1 ShellNameCount
|
|
LDA.1
|
|
SETD.1 TabLeft
|
|
STA.1
|
|
SETD.3 ShellNames
|
|
tabNameNext:
|
|
SETD.1 TabLeft
|
|
LDA.1
|
|
BRA tabNamesDone
|
|
DECA
|
|
STA.1
|
|
CALL tabOffer
|
|
BRI tabNameNext
|
|
tabNamesDone:
|
|
RET
|
|
|
|
; ---- Showing them when they cannot be narrowed further ----
|
|
;
|
|
; The line is reprinted underneath afterwards, and where it now BEGINS is asked rather than
|
|
; remembered: the prompt has just been printed somewhere else entirely, and the whole of the
|
|
; editor's idea of where things are hangs off those two registers.
|
|
tabList:
|
|
CALL newLine
|
|
INIA 0x01
|
|
SETD.1 TabShowing
|
|
STA.1
|
|
CALL tabRunSources
|
|
RSTA
|
|
SETD.1 TabShowing
|
|
STA.1
|
|
CALL newLine
|
|
CALL sayPrompt
|
|
|
|
INA 0x03
|
|
SETD.1 EditRow
|
|
STA.1
|
|
INA 0x04
|
|
SETD.1 EditColumn
|
|
STA.1
|
|
RSTA
|
|
SETD.1 EditDrawn
|
|
STA.1 ; Nothing of the line is on the screen at its new place.
|
|
CALL editRedraw
|
|
BRI editKey
|
|
|
|
; ---- Things a line can be given a name for ----
|
|
;
|
|
; Eight of them, so a script can hold a path or a half written command and compose the rest
|
|
; around it later. Each is one record of sixty four bytes: sixteen of name, then forty eight
|
|
; of value. SIXTY FOUR RATHER THAN EIGHTY, because A and B are a sixteen bit shift register
|
|
; and two rotations right turn a slot number into the offset of its slot - the same trick the
|
|
; history uses, and the reason neither of them needs a multiply this machine has not got.
|
|
;
|
|
; A slot whose name begins with a zero is free. Nothing counts them and nothing has to be
|
|
; kept in step.
|
|
;
|
|
; ---- A name that was never set is an error, and says so ----
|
|
;
|
|
; Expanding it to nothing is what other shells do and it is the wrong answer here: a mistyped
|
|
; name would quietly become an empty path, and everything else in this system spends its
|
|
; effort on saying what went wrong. Somebody who genuinely wants an empty value writes
|
|
; "set name" with nothing after it and gets one - so the escape hatch exists, and has to be
|
|
; asked for rather than arrived at by accident.
|
|
|
|
; DP3 to variable slot A, which is 0 to 7.
|
|
varSlotAt:
|
|
SETD.3 VarSlots
|
|
RSTB
|
|
SHR
|
|
SHR ; A and B together are the slot number times sixty four.
|
|
DPUW.3
|
|
RET
|
|
|
|
; DP0 names a name. Q is zero if it is there, and DP3 is left on its slot.
|
|
varFind:
|
|
SETD.1 VarWanted
|
|
STD.0.1
|
|
RSTA
|
|
SETD.1 VarSlot
|
|
STA.1
|
|
varFindNext:
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
INIB 0d8
|
|
CCF
|
|
SUB
|
|
BNC varFindNo ; Past the last slot.
|
|
|
|
LDA.1
|
|
CALL varSlotAt
|
|
SETD.1 VarWanted
|
|
LDD.0.1
|
|
|
|
LDA.3
|
|
BRA varFindStep ; A free slot has no name to be the one wanted.
|
|
varFindMatch:
|
|
LDA.0
|
|
BRA varFindEnded
|
|
LDB.3
|
|
XOR
|
|
BNQ varFindStep
|
|
INCD.0
|
|
INCD.3
|
|
BRI varFindMatch
|
|
varFindEnded:
|
|
; The same only if the slot's name ends here too. "path" and "pathname" are two names, and
|
|
; comparing until the shorter runs out would call them one.
|
|
LDA.3
|
|
BNA varFindStep
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
CALL varSlotAt
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: found.
|
|
RET
|
|
|
|
varFindStep:
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI varFindNext
|
|
|
|
varFindNo:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is one: no such name.
|
|
RET
|
|
|
|
; DP3 names a slot. Moves it on to that slot's value.
|
|
varValueOf:
|
|
INIA 0d16
|
|
DPUA.3
|
|
RET
|
|
|
|
; DP0 names a name and DP1 its value. Q is zero if it was written down.
|
|
varSet:
|
|
SETD.2 VarKeepValue
|
|
STD.1.2 ; The value, while a slot is looked for.
|
|
|
|
CALL varFind
|
|
BRQ varSetInto ; Already there, so it is written over.
|
|
|
|
; A free one, which is any whose name begins with a zero.
|
|
RSTA
|
|
SETD.1 VarSlot
|
|
STA.1
|
|
varSetFree:
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
INIB 0d8
|
|
CCF
|
|
SUB
|
|
BNC varSetFull
|
|
LDA.1
|
|
CALL varSlotAt
|
|
LDA.3
|
|
BRA varSetInto
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI varSetFree
|
|
|
|
varSetInto:
|
|
; Which slot it is, is in VarSlot either way: varFind leaves the one it found there, and
|
|
; the search above leaves the free one it stopped on. So it is worked out again rather than
|
|
; carried in a pointer through two calls that would each put it back.
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
CALL varSlotAt
|
|
PSHD.3
|
|
POPD.1
|
|
INIA 0d15
|
|
CALL varCopy ; DP0 is still the name.
|
|
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
CALL varSlotAt
|
|
CALL varValueOf
|
|
PSHD.3
|
|
POPD.1
|
|
SETD.2 VarKeepValue
|
|
LDD.0.2
|
|
INIA 0d47
|
|
CALL varCopy
|
|
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: it is written down.
|
|
RET
|
|
|
|
varSetFull:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is one: there is no room for another.
|
|
RET
|
|
|
|
; DP0 to DP1, at most A characters and then the zero that ends them.
|
|
varCopy:
|
|
SETD.2 VarRoom
|
|
STA.2
|
|
varCopyNext:
|
|
SETD.2 VarRoom
|
|
LDA.2
|
|
BRA varCopyDone
|
|
DECA
|
|
STA.2
|
|
LDA.0
|
|
BRA varCopyDone
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
BRI varCopyNext
|
|
varCopyDone:
|
|
RSTA
|
|
STA.1
|
|
RET
|
|
|
|
; A holds a character. Q is zero if it is one a name may be made of, which is what decides
|
|
; where a name written in the middle of a path stops: "$where/file" is a name and a path.
|
|
varNameChar:
|
|
INIB 0x30
|
|
CCF
|
|
SUB
|
|
BRC varNameCharNo ; Below a digit.
|
|
INIB 0x3A
|
|
CCF
|
|
SUB
|
|
BRC varNameCharYes ; A digit.
|
|
INIB 0x41
|
|
CCF
|
|
SUB
|
|
BRC varNameCharNo
|
|
INIB 0x5B
|
|
CCF
|
|
SUB
|
|
BRC varNameCharYes ; A capital.
|
|
INIB 0x61
|
|
CCF
|
|
SUB
|
|
BRC varNameCharNo
|
|
INIB 0x7B
|
|
CCF
|
|
SUB
|
|
BRC varNameCharYes ; A small letter.
|
|
varNameCharNo:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
varNameCharYes:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- The line, with every name in it replaced by what it stands for ----
|
|
;
|
|
; Done here, on its way from wherever it was read to whoever runs it, so that a script line
|
|
; and a typed line behave the same and no command has to know that variables exist at all.
|
|
; The same shape as the line editing: one place the whole system already flows through.
|
|
;
|
|
; WHERE THE OUTPUT HAS GOT TO IS KEPT IN MEMORY rather than in a pointer. There are four
|
|
; pointers and this needs the line being read, the name being gathered, the value being
|
|
; copied and somewhere to put each character - and the moment the output position lived in
|
|
; one of them, gathering a name reset it to the start of the line.
|
|
;
|
|
; Q is zero if the line is ready to run.
|
|
varExpand:
|
|
SETD.0 VarLine
|
|
SETD.1 VarPut
|
|
STD.0.1
|
|
RSTA
|
|
SETD.1 VarUsed
|
|
STA.1
|
|
|
|
SETD.0 CommandLine
|
|
varExpandNext:
|
|
LDA.0
|
|
BRA varExpandDone
|
|
INIB 0x24
|
|
XOR
|
|
BRQ varExpandName
|
|
CALL varExpandPut
|
|
BNQ varExpandLong
|
|
INCD.0
|
|
BRI varExpandNext
|
|
|
|
varExpandName:
|
|
; Past the marker, and then as much of a name as follows it.
|
|
INCD.0
|
|
SETD.2 VarName
|
|
RSTA
|
|
SETD.1 VarNameLen
|
|
STA.1
|
|
varExpandNameChar:
|
|
LDA.0
|
|
CALL varNameChar
|
|
BNQ varExpandNamed
|
|
SETD.1 VarNameLen
|
|
LDA.1
|
|
INIB 0d15
|
|
CCF
|
|
SUB
|
|
BNC varExpandNameLong ; One more and there would be nowhere to put it.
|
|
LDA.0
|
|
STA.2
|
|
INCD.2
|
|
INCD.0
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI varExpandNameChar
|
|
|
|
varExpandNamed:
|
|
RSTA
|
|
STA.2 ; The zero that ends the gathered name.
|
|
|
|
SETD.1 VarNameLen
|
|
LDA.1
|
|
BNA varExpandLookUp
|
|
|
|
; A marker with nothing after it is just a marker, and goes through as one rather than
|
|
; being an error about a variable nobody mentioned.
|
|
INIA 0x24
|
|
CALL varExpandPut
|
|
BNQ varExpandLong
|
|
BRI varExpandNext
|
|
|
|
varExpandLookUp:
|
|
SETD.1 VarKeepLine
|
|
STD.0.1 ; Where the line has got to, across the search.
|
|
SETD.0 VarName
|
|
CALL varFind
|
|
BNQ varExpandNoSuch
|
|
CALL varValueOf
|
|
SETD.1 VarKeepLine
|
|
LDD.0.1
|
|
|
|
varExpandValue:
|
|
LDA.3
|
|
BRA varExpandNext
|
|
CALL varExpandPut
|
|
BNQ varExpandLong
|
|
INCD.3
|
|
BRI varExpandValue
|
|
|
|
varExpandDone:
|
|
RSTA
|
|
CALL varExpandPut ; The zero that ends the built line.
|
|
BNQ varExpandLong
|
|
|
|
; And back where everything downstream looks for it.
|
|
SETD.0 VarLine
|
|
SETD.1 CommandLine
|
|
INIA 0d127
|
|
CALL varCopy
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; A holds a character. Puts it where the built line has got to. Q is one if there is no room,
|
|
; which is a line that grew past what the shell can hold once its names were filled in.
|
|
varExpandPut:
|
|
SETD.2 VarHold
|
|
STA.2
|
|
SETD.2 VarUsed
|
|
LDA.2
|
|
INIB 0d127
|
|
CCF
|
|
SUB
|
|
BNC varExpandNoRoom
|
|
|
|
SETD.2 VarPut
|
|
LDD.1.2
|
|
SETD.2 VarHold
|
|
LDA.2
|
|
STA.1
|
|
INCD.1
|
|
SETD.2 VarPut
|
|
STD.1.2
|
|
|
|
SETD.2 VarUsed
|
|
LDA.2
|
|
INCA
|
|
STA.2
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
varExpandNoRoom:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- A name too long to be one is not a shorter name ----
|
|
;
|
|
; Cutting it off at fifteen characters is what this did, and it is the quiet wrong answer
|
|
; this whole feature was built to avoid: two names that differ only after the fifteenth
|
|
; character would be ONE variable, and the message about a missing one would name something
|
|
; the person never typed.
|
|
varExpandNameLong:
|
|
SETD.0 VarNameLong
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
varExpandLong:
|
|
SETD.0 VarTooLong
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
varExpandNoSuch:
|
|
SETD.0 VarNoSuch
|
|
CALL printString
|
|
SETD.0 VarName
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; ---- Lines that are only run sometimes ----
|
|
;
|
|
; "if" takes a COMMAND and runs it, and what follows is run only if that command worked. That
|
|
; is the Bourne shell's answer and it is the reason "test" exists there: one rule in if, and
|
|
; comparing two things is just another command that can fail. Here that command is "same",
|
|
; and the shell already had the other half - LineFailed, which every command sets and which
|
|
; stop-on-failure was built on.
|
|
;
|
|
; A block is a record of sixteen bytes, eight of them, and sixteen because A and B are a
|
|
; shift register: four rotations turn a block number into its offset. The history and the
|
|
; variables are addressed the same way and for the same reason.
|
|
;
|
|
; +0 state: 0 running, 1 not but an else would, 2 not and an else would not
|
|
; +1 kind: 0 if, 1 while, 2 for
|
|
; +2 how many words a for has used
|
|
; +4 where the line that opened it was, as a script position - see blockKeepWhere
|
|
;
|
|
; The two kinds of not-running are what make nesting work without looking down the stack: an
|
|
; "if" met while something above it is being skipped pushes a 2, so the top of the stack
|
|
; always says everything, and a line runs when the stack is empty or its top is nought.
|
|
|
|
; DP3 to block A.
|
|
blockAt:
|
|
SETD.3 BlockStack
|
|
RSTB
|
|
SHR
|
|
SHR
|
|
SHR
|
|
SHR
|
|
DPUW.3
|
|
RET
|
|
|
|
; DP3 on the block on top. Q is one if there is not one.
|
|
blockTop:
|
|
SETD.1 BlockDepth
|
|
LDA.1
|
|
BRA blockNone
|
|
DECA
|
|
CALL blockAt
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
blockNone:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
blockSkipping:
|
|
CALL blockTop
|
|
BNQ blockRunning ; Nothing open, so nothing is being skipped.
|
|
LDA.3
|
|
BRA blockRunning
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is one: skipping.
|
|
RET
|
|
blockRunning:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: running.
|
|
RET
|
|
|
|
; A holds the state to push and LoopKind what kind of block it is. Q is one if there is no
|
|
; room for another.
|
|
blockPush:
|
|
SETD.1 BlockHold
|
|
STA.1
|
|
SETD.1 BlockDepth
|
|
LDA.1
|
|
INIB 0d8
|
|
CCF
|
|
SUB
|
|
BNC blockFull
|
|
|
|
LDA.1
|
|
CALL blockAt
|
|
SETD.1 BlockHold
|
|
LDA.1
|
|
STA.3
|
|
INCD.3
|
|
SETD.1 LoopKind
|
|
LDA.1
|
|
STA.3
|
|
INCD.3
|
|
RSTA
|
|
STA.3 ; No words used yet, whatever kind it is.
|
|
|
|
SETD.1 BlockDepth
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
blockFull:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
blockPop:
|
|
SETD.1 BlockDepth
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
RET
|
|
|
|
; ---- Going back to the line that opened a block ----
|
|
;
|
|
; A loop is a block that, when it ends, puts the reader back where it started. The position
|
|
; was kept before the line was read - see ScriptLineIndex in script.asm, because by the time
|
|
; a line has been read the reader is past it and a line is not a fixed size to subtract. The
|
|
; block it names is read again on the way back, which is what makes the pointer into it mean
|
|
; what it meant. Exactly what nesting one script inside another already does, for a different
|
|
; reason.
|
|
;
|
|
; DP3 names the block.
|
|
blockKeepWhere:
|
|
SETD.0 ScriptLineIndex
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d4
|
|
CALL sbfsCopyWord
|
|
SETD.0 ScriptLineAt
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d6
|
|
CALL sbfsCopyWord
|
|
SETD.0 ScriptLineBlocks
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d8
|
|
CALL sbfsCopyWord
|
|
RET
|
|
|
|
blockGoWhere:
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d4
|
|
SETD.1 ScriptIndex
|
|
CALL sbfsCopyWord
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d6
|
|
SETD.1 ScriptAt
|
|
CALL sbfsCopyWord
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d8
|
|
SETD.1 ScriptBlocks
|
|
CALL sbfsCopyWord
|
|
CALL scriptReread
|
|
RET
|
|
|
|
; ---- if ----
|
|
;
|
|
; The rest of the line is a command, so the line becomes that command and is dispatched
|
|
; again, with a note saying that what it makes of it decides a block rather than being
|
|
; somebody's answer. The note is read at the top of the loop, which is where every command
|
|
; comes back to and the one place that sees a result before the next line disturbs it.
|
|
doIf:
|
|
RSTA
|
|
SETD.1 LoopKind
|
|
STA.1
|
|
doIfKind:
|
|
CALL blockSkipping
|
|
BNQ ifSkipped
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA ifWhat ; Nothing to decide by.
|
|
|
|
; The rest of the line, moved to the front of it. Forwards, and the source is ahead of the
|
|
; destination, so one walk does it without anything being overwritten before it is read.
|
|
SETD.1 CommandLine
|
|
ifShift:
|
|
LDA.0
|
|
STA.1
|
|
BRA ifShifted
|
|
INCD.0
|
|
INCD.1
|
|
BRI ifShift
|
|
ifShifted:
|
|
INIA 0x01
|
|
SETD.1 IfPending
|
|
STA.1
|
|
BRI promptDispatch
|
|
|
|
ifSkipped:
|
|
; Inside something that is not being taken. The condition is not run and not even looked
|
|
; at - a line that is not being taken must not be able to fail, or a name it mentions
|
|
; would have to exist.
|
|
INIA 0d2
|
|
CALL blockPush
|
|
BNQ ifDeep
|
|
BRI prompt
|
|
|
|
ifWhat:
|
|
SETD.0 IfUsage
|
|
BRI blockComplain
|
|
ifDeep:
|
|
SETD.0 IfTooDeep
|
|
BRI blockComplain
|
|
|
|
; ---- while ----
|
|
;
|
|
; The same shape as if, and then one thing more: the block remembers where the while line
|
|
; was, and end goes back to it - so the condition is ASKED AGAIN rather than remembered.
|
|
doWhile:
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
BRA loopTyped ; There is nothing to go back to at a prompt.
|
|
INIA 0d1
|
|
SETD.1 LoopKind
|
|
STA.1
|
|
BRI doIfKind
|
|
|
|
; Neither loop means anything typed at a prompt: there is no line to go back to. Said once,
|
|
; because both of them arrive here and a message naming the wrong one is worse than none.
|
|
loopTyped:
|
|
SETD.0 LoopTyped
|
|
BRI blockComplain
|
|
|
|
; ---- else ----
|
|
doElse:
|
|
CALL blockTop
|
|
BNQ elseLonely
|
|
LDA.3
|
|
BRA elseTaken ; This branch ran, so the other one does not.
|
|
INIB 0d1
|
|
XOR
|
|
BNQ elseStays ; A two stays a two: nothing here is being taken.
|
|
RSTA
|
|
STA.3
|
|
BRI prompt
|
|
elseTaken:
|
|
INIA 0d2
|
|
STA.3
|
|
elseStays:
|
|
BRI prompt
|
|
elseLonely:
|
|
SETD.0 ElseLonely
|
|
BRI blockComplain
|
|
|
|
; ---- end ----
|
|
;
|
|
; What it does depends on what it closes. An if is simply taken away. A loop that was running
|
|
; goes back to the line that opened it - a while so that its condition is asked again, and a
|
|
; for so that its next word is taken - and a loop that was not running has finished, so it
|
|
; goes away like anything else.
|
|
doEnd:
|
|
CALL blockTop
|
|
BNQ endLonely
|
|
|
|
LDA.3
|
|
BNA endAway ; Not running, so whatever it was, it is over.
|
|
|
|
INCD.3
|
|
LDA.3
|
|
BRA endAway ; An if that ran needs nothing doing.
|
|
|
|
INIB 0d2
|
|
XOR
|
|
BRQ endForAgain
|
|
|
|
; A while: taken away, and the reader put back on its line. The line pushes a fresh block
|
|
; when it asks its question again.
|
|
CALL blockTop
|
|
CALL blockGoWhere
|
|
CALL blockPop
|
|
BRI prompt
|
|
|
|
endForAgain:
|
|
; A for: the block stays, because what it has used is in it, and the line it opened with
|
|
; reads itself again and counts off one more word.
|
|
INIA 0x01
|
|
SETD.1 LoopResume
|
|
STA.1
|
|
CALL blockTop
|
|
CALL blockGoWhere
|
|
BRI prompt
|
|
|
|
endAway:
|
|
CALL blockPop
|
|
BRI prompt
|
|
endLonely:
|
|
SETD.0 EndLonely
|
|
BRI blockComplain
|
|
|
|
; ---- for ----
|
|
;
|
|
; "for x in a b c" runs what follows once for each word, with x set to it in turn. THE LINE IS
|
|
; READ AGAIN ON EVERY TURN and the words counted off from the front, so the only thing a block
|
|
; has to remember is how many have been used - one byte, rather than a copy of the list in
|
|
; every block.
|
|
doFor:
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
BRA loopTyped
|
|
|
|
SETD.1 LoopResume
|
|
LDA.1
|
|
BNA forAgain
|
|
|
|
CALL blockSkipping
|
|
BNQ ifSkipped
|
|
|
|
; A new one. It is pushed running, and set to not-running below if there are no words.
|
|
INIA 0d2
|
|
SETD.1 LoopKind
|
|
STA.1
|
|
RSTA
|
|
CALL blockPush
|
|
BNQ ifDeep
|
|
CALL blockTop
|
|
BNQ forLonely
|
|
CALL blockKeepWhere
|
|
BRI forWord
|
|
|
|
forAgain:
|
|
; Coming round again: the block that is open already says how many words have gone.
|
|
RSTA
|
|
SETD.1 LoopResume
|
|
STA.1
|
|
CALL blockTop
|
|
BNQ forLonely
|
|
INIA 0d2
|
|
DPUA.3
|
|
LDA.3
|
|
INCA
|
|
STA.3
|
|
|
|
forWord:
|
|
CALL blockTop
|
|
BNQ forLonely
|
|
INIA 0d2
|
|
DPUA.3
|
|
LDA.3
|
|
SETD.1 ForUsed
|
|
STA.1
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
SETD.1 ForVarAt
|
|
STD.0.1
|
|
CALL textSplit
|
|
|
|
; The word "in" between the name and the words. Split off first and then compared, because
|
|
; textSame asks whether two whole strings are the same and "in red green blue" is not "in".
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA forWhat
|
|
CALL textSplit
|
|
SETD.1 InName
|
|
CALL textSame
|
|
BNQ forWhat
|
|
|
|
forSkipWords:
|
|
SETD.1 ForUsed
|
|
LDA.1
|
|
BRA forTakeWord
|
|
DECA
|
|
STA.1
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA forNoMore
|
|
CALL textSplit
|
|
BRI forSkipWords
|
|
|
|
forTakeWord:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA forNoMore
|
|
SETD.1 ForWordAt
|
|
STD.0.1
|
|
CALL textSplit
|
|
|
|
SETD.0 ForVarAt
|
|
LDD.0.0
|
|
SETD.1 ForWordAt
|
|
LDD.1.1
|
|
CALL varSet
|
|
BNQ forFull
|
|
|
|
CALL blockTop
|
|
BNQ forLonely
|
|
RSTA
|
|
STA.3 ; Running, and the body follows.
|
|
BRI prompt
|
|
|
|
forNoMore:
|
|
; The words have run out, so the body is passed over and the end takes the block away.
|
|
CALL blockTop
|
|
BNQ forLonely
|
|
INIA 0d1
|
|
STA.3
|
|
BRI prompt
|
|
|
|
forWhat:
|
|
SETD.0 ForUsage
|
|
BRI blockComplain
|
|
forFull:
|
|
SETD.0 SetVarNoRoom
|
|
BRI blockComplain
|
|
forLonely:
|
|
SETD.0 EndLonely
|
|
BRI blockComplain
|
|
|
|
; ---- same ----
|
|
;
|
|
; Two words, and it fails when they differ. A command rather than a form of if, so that if
|
|
; has one rule and anything that can fail can be asked about.
|
|
doSame:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
SETD.1 SameFirst
|
|
STD.0.1
|
|
CALL textSplit
|
|
|
|
SETD.0 SameFirst
|
|
LDD.0.0
|
|
SETD.1 TextRest
|
|
LDD.1.1
|
|
CALL textSame
|
|
BNQ sameNot
|
|
BRI prompt
|
|
sameNot:
|
|
BRI commandFailed
|
|
|
|
; DP0 names what went wrong. Says it and marks the line, which is what every one of these
|
|
; does and is worth writing once.
|
|
blockComplain:
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; Takes the spaces off the front of the line, by moving what is after them to the front. In
|
|
; place and forwards, with the source ahead of the destination, so one walk does it.
|
|
lineTrim:
|
|
SETD.0 CommandLine
|
|
LDA.0
|
|
INIB 0x20
|
|
XOR
|
|
BNQ lineTrimDone ; It does not start with one, which is almost every line.
|
|
|
|
lineTrimSkip:
|
|
INCD.0
|
|
LDA.0
|
|
BRA lineTrimEmpty
|
|
INIB 0x20
|
|
XOR
|
|
BRQ lineTrimSkip
|
|
|
|
SETD.1 CommandLine
|
|
lineTrimMove:
|
|
LDA.0
|
|
STA.1
|
|
BRA lineTrimDone
|
|
INCD.0
|
|
INCD.1
|
|
BRI lineTrimMove
|
|
|
|
lineTrimEmpty:
|
|
; Nothing but spaces. It becomes an empty line, which the shell already knows to do
|
|
; nothing about.
|
|
SETD.0 CommandLine
|
|
RSTA
|
|
STA.0
|
|
lineTrimDone:
|
|
RET
|
|
|
|
; DP0 names a line and DP1 a word. Q is zero if the line's FIRST WORD is that word - which
|
|
; is what lets a line be recognised without textSplit writing a zero into the middle of it,
|
|
; and a line that is being skipped must come through untouched.
|
|
lineFirstIs:
|
|
LDA.1
|
|
BRA lineFirstEnded
|
|
LDB.0
|
|
XOR
|
|
BNQ lineFirstNo
|
|
INCD.0
|
|
INCD.1
|
|
BRI lineFirstIs
|
|
lineFirstEnded:
|
|
LDA.0
|
|
BRA lineFirstYes
|
|
INIB 0x20
|
|
XOR
|
|
BRQ lineFirstYes
|
|
lineFirstNo:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
lineFirstYes:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- A line inside a block that is not being taken ----
|
|
;
|
|
; Only the three words that shape a block mean anything here, and they are matched WITHOUT
|
|
; the line being split or its names filled in: a line nobody is running must not be able to
|
|
; fail, and "$whatever" in a branch that was not taken is not a mistake.
|
|
blockPassOver:
|
|
SETD.0 CommandLine
|
|
SETD.1 IfName
|
|
CALL lineFirstIs
|
|
BRQ ifSkipped
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 WhileName
|
|
CALL lineFirstIs
|
|
BRQ ifSkipped
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ForName
|
|
CALL lineFirstIs
|
|
BRQ ifSkipped
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 ElseName
|
|
CALL lineFirstIs
|
|
BRQ doElse
|
|
|
|
SETD.0 CommandLine
|
|
SETD.1 EndName
|
|
CALL lineFirstIs
|
|
BRQ doEnd
|
|
|
|
BRI prompt
|
|
|
|
; ---- set ----
|
|
;
|
|
; "set name value" writes one down, "set name" gives it an empty value on purpose, and "set"
|
|
; on its own says what is written down already - because a name that fails loudly when it was
|
|
; never set needs somewhere to go and look.
|
|
doSetVar:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA setVarList ; Nothing after the word, so this is asking what is set.
|
|
|
|
; DP0 is still on the rest of the line - reading a byte through a pointer does not move it -
|
|
; so the name starts where it already points.
|
|
SETD.1 SetVarWhich
|
|
STD.0.1
|
|
CALL textSplit
|
|
|
|
SETD.0 SetVarWhich
|
|
LDD.0.0
|
|
SETD.1 TextRest
|
|
LDD.1.1
|
|
CALL varSet
|
|
BNQ setVarFull
|
|
BRI prompt
|
|
|
|
setVarFull:
|
|
SETD.0 SetVarNoRoom
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
setVarList:
|
|
RSTA
|
|
SETD.1 VarSlot
|
|
STA.1
|
|
setVarListNext:
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
INIB 0d8
|
|
CCF
|
|
SUB
|
|
BNC setVarListDone
|
|
LDA.1
|
|
CALL varSlotAt
|
|
LDA.3
|
|
BRA setVarListStep ; A free slot has nothing to say.
|
|
|
|
PSHD.3
|
|
POPD.0
|
|
CALL printString
|
|
SETD.0 SetVarIs
|
|
CALL printString
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
CALL varSlotAt
|
|
CALL varValueOf
|
|
PSHD.3
|
|
POPD.0
|
|
CALL printString
|
|
CALL newLine
|
|
setVarListStep:
|
|
SETD.1 VarSlot
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI setVarListNext
|
|
setVarListDone:
|
|
BRI prompt
|
|
|
|
; ---- A command that did not work ----
|
|
;
|
|
; The one place a failure is recorded, so that the thing reading lines out of a file can
|
|
; tell whether to go on. It says nothing: whatever sent us here has already said what was
|
|
; wrong in words, and a number after that is noise. See LastStatus.
|
|
commandFailed:
|
|
INIA 0x01
|
|
SETD.1 LineFailed
|
|
STA.1
|
|
BRI prompt
|
|
|
|
promptUnknown:
|
|
; Nothing built in matched, so the disk is asked before anybody is told they are wrong. A
|
|
; word this shell does not know is very often the name of a program sitting right there,
|
|
; and looking costs a walk of the directory.
|
|
;
|
|
; THE BUILT-IN COMMANDS ARE TRIED FIRST AND ALWAYS WIN. Nothing that turns up on a disk
|
|
; can quietly become "dir" or "exit", which is what makes those two worth trusting at the
|
|
; moment the disk is the thing being doubted.
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA promptSayUnknown ; No filesystem, so there is nothing to look through.
|
|
|
|
; THREE PLACES, TRIED IN ORDER: where you are, the system's own place for programs on the
|
|
; disk you are on, and then that same place on drive 0. The first is what makes a program
|
|
; you are working on the one that runs; the second is what makes Snake work from anywhere
|
|
; without a copy of it in every directory; the third is what makes it work from a disk of
|
|
; your own, which has your files on it and no system.
|
|
;
|
|
; None of them is stored anywhere, so there is nothing to configure and nothing to go
|
|
; stale.
|
|
RSTA
|
|
SETD.0 NamePrefix
|
|
STA.0
|
|
|
|
; Where the person is, kept so it can be given back. A program is fetched from wherever it
|
|
; lives and then runs on the files of whoever ran it.
|
|
INA 0x24
|
|
SETD.0 SearchDrive
|
|
STA.0
|
|
|
|
promptSearch:
|
|
CALL nameProgram
|
|
SETD.0 NameOk
|
|
LDA.0
|
|
BRA promptElsewhere ; Too long to be a path, so it is not the name of one.
|
|
|
|
CALL loadProgram
|
|
SETD.0 LoadStatus
|
|
LDA.0
|
|
BNA promptNotLoaded
|
|
|
|
; ---- Loaded, so the drive goes back to the person ----
|
|
;
|
|
; The program is in memory now and the block numbers it came from mean nothing any more,
|
|
; which is what makes this safe here and not in the middle of a path. A program fetched
|
|
; from the system disk then runs on the disk its user was standing on - which is the whole
|
|
; point of being able to keep a disk of your own.
|
|
SETD.0 SearchDrive
|
|
LDA.0
|
|
CALL sbfsUse
|
|
BRI runLoaded ; It loaded, and the machine is its now.
|
|
|
|
promptNotLoaded:
|
|
SETD.0 LoadStatus
|
|
LDA.0
|
|
|
|
; No file of that name is not a fault. It is the ordinary case of a word this shell does
|
|
; not know, and it is the only answer worth looking somewhere else for. Anything else
|
|
; means a file of that name IS there and something is wrong with it, and answering
|
|
; "I do not know: Snake" about a Snake.sbx that is sitting on the disk would send
|
|
; somebody looking in the wrong place.
|
|
INIB 0d2
|
|
CCF
|
|
SUB
|
|
BNQ loadFailed
|
|
|
|
promptElsewhere:
|
|
; Was that the last place there is?
|
|
SETD.0 NamePrefix
|
|
LDA.0
|
|
INIB 0d2
|
|
CCF
|
|
SUB
|
|
BRQ promptGaveUp
|
|
|
|
; A word beginning with a separator has said where to look, and looking somewhere else
|
|
; would be answering a different question from the one asked.
|
|
SETD.0 CommandLine
|
|
LDA.0
|
|
INIB 0x2F
|
|
CCF
|
|
SUB
|
|
BRQ promptSayUnknown
|
|
|
|
SETD.0 NamePrefix
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
BRI promptSearch
|
|
|
|
promptGaveUp:
|
|
; Every place has been tried, and the drive is put back before anybody is told anything:
|
|
; the search moved it, and a word the shell does not know should not move somebody either.
|
|
SETD.0 SearchDrive
|
|
LDA.0
|
|
CALL sbfsUse
|
|
|
|
promptSayUnknown:
|
|
; Saying which word was not understood is worth the four instructions: it tells somebody
|
|
; who mistyped what they actually typed.
|
|
SETD.0 Unknown
|
|
CALL printString
|
|
SETD.0 CommandLine
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; Running out of console leaves the cursor part way along a line, because there was no
|
|
; return at the end to move it on. Somebody who typed "exit" has already pressed one, and
|
|
; a second would only leave a blank line behind.
|
|
; Leaving whatever you are in: the monitor if you are in it, and the machine if you are
|
|
; not. Two exits to stop from the monitor, which is what every nested prompt has ever asked
|
|
; for and reads the right way round.
|
|
doExit:
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA quit
|
|
RSTA
|
|
STA.0
|
|
BRI prompt
|
|
|
|
doMonitor:
|
|
INIA 0x01
|
|
SETD.0 Mode
|
|
STA.0
|
|
SETD.0 MonitorHelp
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
quitRanOut:
|
|
CALL newLine
|
|
quit:
|
|
SETD.0 Farewell
|
|
CALL printString
|
|
CALL newLine
|
|
HALT
|
|
|
|
; ---- dir ----
|
|
;
|
|
; Walks the directory and prints what is in it. A free entry in the middle of a directory
|
|
; is stepped over by the walk, so what comes out is the files and nothing else.
|
|
|
|
doDir:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA dirNoDisk
|
|
|
|
RSTA
|
|
SETD.0 DirSeen
|
|
STA.0
|
|
SETD.0 DirFolders
|
|
STA.0
|
|
|
|
CALL sbfsFirst
|
|
BRI dirCheck
|
|
dirStep:
|
|
CALL sbfsNext
|
|
dirCheck:
|
|
BNQ dirDone
|
|
|
|
SETD.0 DirSeen
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
|
|
SETD.0 SbfsName
|
|
CALL printString
|
|
|
|
SETD.0 SbfsName
|
|
CALL nameWidth
|
|
MVQA
|
|
CALL printSpaces
|
|
|
|
; A save that stopped before it committed says so instead of saying a size, because the
|
|
; size it holds is the room it asked for rather than what was written into it. Asked
|
|
; first, since it is the one thing here that is not really a file yet.
|
|
;
|
|
; IT IS SHOWN RATHER THAN HIDDEN, and that is the whole of the recovery this format
|
|
; offers: the bytes are all there under that name, so seeing it is what lets somebody
|
|
; rename it back. Left off the listing it would be blocks nobody could account for.
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x04
|
|
AND
|
|
BNQ dirUnfinished
|
|
|
|
; A directory says so instead of saying a size. It has no blocks, so the arithmetic
|
|
; below would call it a file of no bytes - which is a different thing that happens to
|
|
; look the same from here.
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x02
|
|
AND
|
|
BNQ dirIsDirectory
|
|
|
|
; A file's length is its block count times 256 plus its tail, which is the block count
|
|
; in the high byte and the tail in the low one. Nothing has to multiply anything.
|
|
;
|
|
; THAT ONLY WORKS WHILE THE BLOCK COUNT FITS IN A BYTE. Two hundred and fifty six blocks
|
|
; is sixty five thousand five hundred and thirty six bytes, and the number that comes out
|
|
; of the shift is sixteen bits wide - so a file of that size or more came out as itself
|
|
; less 65536, which is a plausible number and a wrong one. cosmos.asm is 82,996 bytes and
|
|
; this called it 17,460.
|
|
;
|
|
; Such a file says its size in BLOCKS instead. Printing the true figure would want
|
|
; decimal printing twenty four bits wide, which is a page of console.asm to say something
|
|
; nobody reads more precisely than "big"; changing the unit says it exactly and can never
|
|
; be wrong.
|
|
SETD.0 SbfsFileBlocks
|
|
LDA.0
|
|
BNA dirInBlocks
|
|
|
|
SETD.0 SbfsFileBlocks
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 DirSize
|
|
STA.1
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
INCD.1
|
|
STA.1
|
|
|
|
SETD.0 DirSize
|
|
CALL printWordDecimal
|
|
CALL newLine
|
|
BRI dirStep
|
|
|
|
dirInBlocks:
|
|
SETD.0 SbfsFileBlocks
|
|
CALL printWordDecimal
|
|
SETD.0 BlocksText
|
|
CALL printString
|
|
CALL newLine
|
|
BRI dirStep
|
|
|
|
dirUnfinished:
|
|
SETD.0 UnfinishedText
|
|
CALL printString
|
|
CALL newLine
|
|
BRI dirStep
|
|
|
|
dirIsDirectory:
|
|
SETD.0 DirFolders
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
SETD.0 DirectoryText
|
|
CALL printString
|
|
CALL newLine
|
|
BRI dirStep
|
|
|
|
dirDone:
|
|
; Directories were counted alongside the files and now come back out of the total, so
|
|
; that "three files" means three files. Saying it any other way makes the number
|
|
; disagree with the listing right above it, which is the sort of thing that teaches
|
|
; somebody not to trust the listing.
|
|
SETD.0 DirFolders
|
|
LDA.0
|
|
SETD.1 DirTaken
|
|
STA.1
|
|
SETD.0 DirSeen
|
|
LDA.0
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
STA.0
|
|
|
|
SETD.0 DirSeen
|
|
LDA.0
|
|
CALL printByteDecimal
|
|
; One file is not one files. Cheap to get right and it reads as carelessness otherwise.
|
|
SETD.0 DirSeen
|
|
LDA.0
|
|
DECA
|
|
BRA dirOne
|
|
SETD.0 FilesText
|
|
BRI dirCount
|
|
dirOne:
|
|
SETD.0 FileText
|
|
dirCount:
|
|
CALL printString
|
|
|
|
; And how many of them were directories, but only when there were any. A disk with none
|
|
; on it should read exactly the way it always did.
|
|
SETD.0 DirFolders
|
|
LDA.0
|
|
BRA dirNoFolders
|
|
SETD.0 AndText
|
|
CALL printString
|
|
SETD.0 DirFolders
|
|
LDA.0
|
|
CALL printByteDecimal
|
|
SETD.0 DirFolders
|
|
LDA.0
|
|
DECA
|
|
BRA dirOneFolder
|
|
SETD.0 FoldersText
|
|
BRI dirFolderCount
|
|
dirOneFolder:
|
|
SETD.0 FolderText
|
|
dirFolderCount:
|
|
CALL printString
|
|
dirNoFolders:
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
dirNoDisk:
|
|
SETD.0 NoDisk
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; DP0 names a string. Q is how many spaces pad it out to twenty four columns. A name
|
|
; already that long gets one space, so that it cannot run into the number after it.
|
|
nameWidth:
|
|
INIA 0d24
|
|
SETD.1 WidthLeft
|
|
STA.1
|
|
widthLoop:
|
|
LDA.0
|
|
BRA widthDone
|
|
SETD.1 WidthLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BRA widthFloor
|
|
INCD.0
|
|
BRI widthLoop
|
|
widthFloor:
|
|
INIA 0d1
|
|
SETD.1 WidthLeft
|
|
STA.1
|
|
widthDone:
|
|
SETD.1 WidthLeft
|
|
LDA.1
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Making a file name out of a typed word ----
|
|
;
|
|
; Turns what was typed into the name of a file to go and look for. ".sbx" goes on the end
|
|
; unless it is already there, so that "Snake" and "Snake.sbx" both find the same file.
|
|
;
|
|
; THE EXTENSION IS WHAT MAKES A FILE REACHABLE BY NAME. Typing "notes" looks for notes.sbx
|
|
; and typing "notes.txt" looks for notes.txt.sbx, so a text file cannot be started by
|
|
; typing what it is called, whatever is inside it. Only load reaches a file by its whole
|
|
; name, which is why the monitor can still put any file at all in front of itself.
|
|
;
|
|
; NameOk is one if there is a path in ProgramName and zero if the word could not be made
|
|
; into one. What limits it is the buffer, not the format: each NAME along a path is still
|
|
; twenty two characters, and the path walker refuses a longer one rather than cutting it
|
|
; down. Being refused here reads as an unknown command, which is the truth: nothing this
|
|
; shell can reach is called that.
|
|
nameProgram:
|
|
RSTA
|
|
SETD.0 NameOk
|
|
STA.0 ; Not a name until it turns out to be one.
|
|
|
|
SETD.1 ProgramName
|
|
INIB 0d59 ; What is left of the buffer, less the four for the extension.
|
|
|
|
; The system's own place for programs goes on the front, when that is what is being
|
|
; tried. Putting it here rather than pasting it on afterwards is what keeps the ".sbx"
|
|
; test below looking at the end of the whole thing.
|
|
SETD.0 NamePrefix
|
|
LDA.0
|
|
BRA nameFromLine
|
|
; One is the system's place on this disk; two is the same place on drive 0.
|
|
INIB 0d2
|
|
CCF
|
|
SUB
|
|
BRQ nameSystemApps
|
|
SETD.0 AppsPrefix
|
|
BRI namePrefixCopy
|
|
nameSystemApps:
|
|
SETD.0 SystemAppsPrefix
|
|
namePrefixCopy:
|
|
LDA.0
|
|
BRA nameFromLine
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
DECB
|
|
BRI namePrefixCopy
|
|
|
|
nameFromLine:
|
|
SETD.0 CommandLine
|
|
nameCopy:
|
|
LDA.0
|
|
BRA nameCopied
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
DECB
|
|
BNB nameCopy
|
|
RET ; Longer than the buffer holds, so it is not a path either.
|
|
|
|
nameCopied:
|
|
; DP1 is on the byte after the word, which is where an extension would go, and B is what
|
|
; is left. B is written down before anything else wants the registers.
|
|
RSTA
|
|
STA.1
|
|
SETD.0 NameLeft
|
|
STB.0
|
|
|
|
; Only a word of four characters or more can already end in ".sbx". Stepping back four to
|
|
; look at a shorter one would read whatever happens to sit in front of the buffer.
|
|
INIA 0d55
|
|
CCF
|
|
SUB
|
|
BRC nameAppend
|
|
|
|
PSHD.1
|
|
POPD.2
|
|
DPDN.2 0d4
|
|
SETD.0 SbxSuffix
|
|
nameSuffixSame:
|
|
LDA.0
|
|
BRA nameMade ; The suffix ran out with all of it matched.
|
|
LDB.2
|
|
CCF
|
|
SUB
|
|
BNQ nameAppend
|
|
INCD.0
|
|
INCD.2
|
|
BRI nameSuffixSame
|
|
|
|
nameAppend:
|
|
SETD.0 NameLeft
|
|
LDA.0
|
|
INIB 0d4
|
|
CCF
|
|
SUB
|
|
BRC nameDone ; Not four characters of room, so there is no name to be made.
|
|
|
|
; The suffix carries its own zero, so copying it to the end of the word ends the word.
|
|
SETD.0 SbxSuffix
|
|
nameSuffixCopy:
|
|
LDA.0
|
|
STA.1
|
|
BRA nameMade
|
|
INCD.0
|
|
INCD.1
|
|
BRI nameSuffixCopy
|
|
|
|
nameMade:
|
|
INIA 0x01
|
|
SETD.0 NameOk
|
|
STA.0
|
|
nameDone:
|
|
RET
|
|
|
|
; ---- load ----
|
|
;
|
|
; Reads a program off the disk and puts it where its header asks to go. Nothing relocates
|
|
; anything: the addresses in the header are the ones the program was built for, and it
|
|
; would not work anywhere else.
|
|
;
|
|
; The whole file is staged at 0x8000 first and then blitted into place, because where the
|
|
; pieces belong is not known until the header has been read, and the header is in the file.
|
|
;
|
|
; The command is a thin thing over loadProgram, which is a subroutine because typing a
|
|
; program's name loads it too and neither caller should own the loading. What differs
|
|
; between them is only what a fault means: "load Snake.sbx" on a disk without it has been
|
|
; given a wrong name, and "Snake" on the same disk has typed a word this shell does not
|
|
; know.
|
|
|
|
doLoad:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA loadNothingNamed
|
|
|
|
; The path exactly as typed. load is how a file is reached by its whole name, so nothing
|
|
; is added to it and nothing is assumed about what it ends in.
|
|
SETD.1 ProgramName
|
|
INIB 0d64
|
|
CALL copyText
|
|
|
|
CALL loadProgram
|
|
SETD.0 LoadStatus
|
|
LDA.0
|
|
BNA loadFailed
|
|
|
|
SETD.0 LoadedText
|
|
CALL printString
|
|
SETD.0 LoadedEntry
|
|
CALL printWordHex
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
loadFailed:
|
|
SETD.1 LoadMessage
|
|
LDD.0.1
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
loadNothingNamed:
|
|
SETD.0 LoadWhat
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; ---- loadProgram ----
|
|
;
|
|
; The name is in ProgramName. LoadStatus says what happened and LoadMessage names the text
|
|
; for it, because a subroutine cannot hand anything back in a register that a RET puts
|
|
; back, and here there are two things to hand back:
|
|
;
|
|
; 0 loaded, and LoadedEntry says where it starts
|
|
; 1 no filesystem on the disk
|
|
; 2 no file of that name
|
|
; 3 the disk would not read it
|
|
; 4 it is not a program
|
|
; 5 a version of the format this loader does not know
|
|
; 6 more vectors than there is room to keep
|
|
; 7 it is a directory
|
|
;
|
|
; Two is the one worth telling apart from the others. It is the only outcome where nothing
|
|
; was wrong with the disk or with a file, and so the only one a caller can fairly report as
|
|
; something other than a fault.
|
|
|
|
loadProgram:
|
|
RSTA
|
|
SETD.0 LoadStatus
|
|
STA.0
|
|
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA loadNoDisk
|
|
|
|
SETD.0 ProgramName
|
|
CALL sbfsFind
|
|
BNQ loadMissing
|
|
|
|
; A DIRECTORY IS REFUSED HERE AND NOT LEFT TO THE MAGIC CHECK BELOW. It has no blocks,
|
|
; so reading it reads nothing and leaves the staging area holding whatever was staged
|
|
; last - which, if that was a program, still says "SBEX" and still has a working entry
|
|
; address in it. Loading a directory would quietly hand back the program before it, and
|
|
; running it would look like the directory had run.
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x02
|
|
AND
|
|
BNQ loadIsDirectory
|
|
|
|
SETD.1 0x80 0x00
|
|
CALL sbfsRead
|
|
BNQ loadUnreadable
|
|
|
|
; "SBEX", or this is not a program. Without this, loading a text file would put nonsense
|
|
; into Program Memory and then jump into the middle of it.
|
|
SETD.0 0x80 0x00
|
|
SETD.2 ExecMagic
|
|
INIA 0d4
|
|
SETD.1 LoadCount
|
|
STA.1
|
|
loadMagicLoop:
|
|
LDA.0
|
|
LDB.2
|
|
XOR
|
|
BNQ loadNotProgram
|
|
INCD.0
|
|
INCD.2
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA loadMagicLoop
|
|
|
|
; Version one is code and data. Version two also brings vectors, which is a thing a
|
|
; loader has to know how to do rather than a detail it can skip: a program whose handlers
|
|
; were quietly dropped would run and then go wrong somewhere with nothing to connect it
|
|
; back to here. Anything else is refused.
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d04
|
|
LDA.0
|
|
SETD.1 LoadVersion
|
|
STA.1
|
|
INIB 0d1
|
|
XOR
|
|
BRQ loadVersionKnown
|
|
SETD.1 LoadVersion
|
|
LDA.1
|
|
INIB 0d2
|
|
XOR
|
|
BNQ loadWrongVersion
|
|
loadVersionKnown:
|
|
|
|
; The code. It comes from the staging area just past the sixteen byte header, and goes
|
|
; wherever the header says, in Program Memory, which the instruction set cannot write
|
|
; and the controller can.
|
|
INIA 0d1
|
|
OUTA 0xE0 ; SourceBank: Data Memory, where the file was staged.
|
|
INIA 0x80
|
|
OUTA 0xE1
|
|
INIA 0d16
|
|
OUTA 0xE2 ; 0x8010, the first byte after the header.
|
|
|
|
RSTA
|
|
OUTA 0xE3 ; DestBank: Program Memory.
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d06
|
|
LDA.0
|
|
OUTA 0xE4
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE5
|
|
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d10
|
|
LDA.0
|
|
OUTA 0xE6
|
|
INCD.0
|
|
LDB.0
|
|
OUTB 0xE7
|
|
|
|
; ---- Nothing to move is not the same as everything to move ----
|
|
;
|
|
; A LENGTH OF ZERO ASKS THE CONTROLLER FOR THE WHOLE 64K, which is the machine's rule and
|
|
; a sensible one for a length somebody typed: two bytes cannot say 65536, and a transfer
|
|
; of no bytes is not usually what anybody meant. It is exactly what is meant here. A
|
|
; segment can genuinely be empty - a five instruction program that only writes to a port
|
|
; has no data at all - and this asked to move 65536 bytes into a bank that has not got
|
|
; them, so the controller refused and the machine stopped in the middle of loading.
|
|
;
|
|
; The header says how long the segment is, so the loader knows before it asks. It has
|
|
; both bytes of the length in hand here, so testing them costs one instruction.
|
|
;
|
|
; NOTHING IS LOST BY SKIPPING IT. A blit leaves the controller's addresses past whatever
|
|
; it touched, and a blit of nothing would have left them exactly where they are.
|
|
OR
|
|
BRQ loadNoCode
|
|
|
|
INIA 0x01
|
|
OUTA 0xE8 ; Blit.
|
|
loadNoCode:
|
|
|
|
; Then the data. A blit leaves its addresses past whatever it touched, so the source is
|
|
; already sitting on the first byte of the data and only the destination changes.
|
|
INIA 0d1
|
|
OUTA 0xE3 ; DestBank: Data Memory.
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d12
|
|
LDA.0
|
|
OUTA 0xE4
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE5
|
|
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d14
|
|
LDA.0
|
|
OUTA 0xE6
|
|
INCD.0
|
|
LDB.0
|
|
OUTB 0xE7
|
|
|
|
; And the same for the data, which is the segment that is actually empty in practice.
|
|
OR
|
|
BRQ loadNoData
|
|
|
|
INIA 0x01
|
|
OUTA 0xE8 ; Blit.
|
|
loadNoData:
|
|
|
|
; ---- The vectors it brought ----
|
|
;
|
|
; Kept here rather than installed. A vector points into a program, so it has no business
|
|
; being in the table while that program is only loaded and not running: run puts them in
|
|
; and exit takes them out again, so the window they are live in is exactly the run.
|
|
; Keeping our own copy is also what lets a program be run more than once, since the
|
|
; staging area it came in on is fair game for the program's own use.
|
|
;
|
|
; Where to read them from is not worked out. The data blit left the controller's source
|
|
; address on the first byte after the data, which is where they are, so it is read back.
|
|
SETD.0 VectorSource
|
|
INA 0xE1
|
|
STA.0
|
|
INCD.0
|
|
INA 0xE2
|
|
STA.0
|
|
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d05
|
|
LDA.0
|
|
SETD.1 LoadedVectorCount
|
|
STA.1
|
|
BRA loadVectorsCopied
|
|
|
|
; More than there is room for is refused rather than half taken. Half a program's
|
|
; handlers is not a smaller version of that program.
|
|
INIB 0d17
|
|
CCF
|
|
SUB
|
|
BNC loadTooManyVectors
|
|
|
|
SETD.0 VectorSource
|
|
LDD.2.0 ; DP2 walks the entries where they are staged.
|
|
SETD.3 LoadedVectors ; DP3 walks our own copy of them.
|
|
SETD.1 LoadedVectorCount
|
|
LDA.1
|
|
SETD.1 VectorsLeft
|
|
STA.1
|
|
|
|
loadVectorCopy:
|
|
; Four bytes: where it goes, then what goes there. The two bytes for what was there
|
|
; before are left alone until something is actually put in.
|
|
LDA.2
|
|
STA.3
|
|
INCD.2
|
|
INCD.3
|
|
LDA.2
|
|
STA.3
|
|
INCD.2
|
|
INCD.3
|
|
LDA.2
|
|
STA.3
|
|
INCD.2
|
|
INCD.3
|
|
LDA.2
|
|
STA.3
|
|
INCD.2
|
|
INCD.3
|
|
INCD.3
|
|
INCD.3
|
|
|
|
SETD.1 VectorsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA loadVectorCopy
|
|
|
|
loadVectorsCopied:
|
|
|
|
; Where it starts. Written out by hand rather than through a routine, because a routine
|
|
; could not hand two bytes back: CALL puts A, B and the first three pointers back the
|
|
; way it found them.
|
|
SETD.0 0x80 0x00
|
|
DPUP.0 0d08
|
|
LDA.0
|
|
SETD.1 LoadedEntry
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
LDA.0
|
|
STA.1
|
|
|
|
INIA 0x01
|
|
SETD.0 LoadedOk
|
|
STA.0
|
|
RET ; LoadStatus is still the zero it was started at.
|
|
|
|
; Which of the seven happened, and where the words for it are. The two are set together so
|
|
; that no caller has to know both, and so that adding a way to fail cannot leave one of
|
|
; them behind.
|
|
loadNoDisk:
|
|
INIA 0d1
|
|
SETD.0 NoDisk
|
|
BRI loadRefuse
|
|
loadMissing:
|
|
INIA 0d2
|
|
SETD.0 NoSuchFile
|
|
BRI loadRefuse
|
|
loadUnreadable:
|
|
INIA 0d3
|
|
SETD.0 Unreadable
|
|
BRI loadRefuse
|
|
loadNotProgram:
|
|
INIA 0d4
|
|
SETD.0 NotProgram
|
|
BRI loadRefuse
|
|
loadWrongVersion:
|
|
INIA 0d5
|
|
SETD.0 WrongVersion
|
|
BRI loadRefuse
|
|
loadTooManyVectors:
|
|
INIA 0d6
|
|
SETD.0 TooManyVectors
|
|
BRI loadRefuse
|
|
loadIsDirectory:
|
|
INIA 0d7
|
|
SETD.0 IsDirectory
|
|
loadRefuse:
|
|
SETD.1 LoadMessage
|
|
STD.0.1
|
|
SETD.1 LoadStatus
|
|
STA.1
|
|
RET
|
|
|
|
; ---- Writing a file a block at a time ----
|
|
;
|
|
; Three handlers over the three routines in sbfs.asm, and thin, because everything that is
|
|
; difficult about writing safely is down there where it is written once.
|
|
;
|
|
; DP0 names the file, DP3 is how many whole blocks and A is what is left over in the last
|
|
; one - the size said the way an entry says it, which is what lets this reach past the
|
|
; 65,535 bytes osFileSave can describe.
|
|
handleFileStart:
|
|
SETD.2 DiskReady
|
|
PSHA
|
|
LDA.2
|
|
BRA fileStartNoDisk
|
|
POPA
|
|
|
|
; The tail is in A and the block count is in DP3, so the count has to come out of the
|
|
; pointer before anything else wants it.
|
|
SETD.2 SbfsFileTail
|
|
STA.2
|
|
|
|
; The low byte comes off the Stack first, which is the same way round Type and every
|
|
; other reader takes a count out of DP3. Popping them the other way gave a block count
|
|
; of the size times two hundred and fifty six, and a start that could find no room.
|
|
PSHD.3
|
|
POPB
|
|
POPA
|
|
SETD.2 SbfsFileBlocks
|
|
STA.2
|
|
INCD.2
|
|
STB.2
|
|
|
|
; What a name means on the disk is about to change, so the remembered file goes.
|
|
CALL fileForget
|
|
CALL sbfsStreamStart
|
|
SRET
|
|
|
|
fileStartNoDisk:
|
|
POPA
|
|
INIA 0d1
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; DP1 is where the block comes from, and A and B together say which block of the file it
|
|
; is, counting from zero - the same way osFileBlock is told which one to fetch.
|
|
handleFileWrite:
|
|
SETD.2 SbfsIndex
|
|
STA.2
|
|
INCD.2
|
|
STB.2
|
|
CALL sbfsStreamWrite
|
|
SRET
|
|
|
|
; DP1 is where the block goes, and A and B together say which one, the same way writing is
|
|
; told. Reads back a block of the file being written.
|
|
handleFileFetch:
|
|
SETD.2 SbfsIndex
|
|
STA.2
|
|
INCD.2
|
|
STB.2
|
|
CALL sbfsStreamFetch
|
|
SRET
|
|
|
|
; DP3 is how many whole blocks it came to and A is what is left over, told the same way
|
|
; osFileStart is told. It need not be what was asked for: a writer that cannot know its
|
|
; size until the last byte asks for enough at the start, where running out costs nothing,
|
|
; and says the truth here. The blocks it did not use go back.
|
|
;
|
|
; This is the only step that can lose anything, and the last one.
|
|
handleFileDone:
|
|
SETD.2 SbfsFileTail
|
|
STA.2
|
|
PSHD.3
|
|
POPB
|
|
POPA
|
|
SETD.2 SbfsFileBlocks
|
|
STA.2
|
|
INCD.2
|
|
STB.2
|
|
CALL fileForget
|
|
CALL sbfsStreamDone
|
|
SRET
|
|
|
|
; ---- osChangeDir ----
|
|
;
|
|
; The service behind the shell's cd, and the reason the shell bothers to put the working
|
|
; directory back when a program stops: without something a program can call, that promise
|
|
; would have been about a thing that could not happen.
|
|
;
|
|
; DP0 names a directory. Q is zero if the machine is now in it.
|
|
handleChangeDir:
|
|
; DP0 arrives holding the path, because an interrupt frame keeps the caller's registers
|
|
; and this runs with them still in place. What it hands BACK has to be written into the
|
|
; frame, since RETI puts every register back the way the caller had it - which is why Q
|
|
; is stored at DP2 plus two below rather than simply being set.
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA changeDirNo
|
|
|
|
CALL sbfsWalk
|
|
BNQ changeDirNo
|
|
|
|
; The root has no entry to ask about, and is always somewhere that can be stood in.
|
|
SETD.2 SbfsAt
|
|
LDA.2
|
|
INCD.2
|
|
LDB.2
|
|
OR
|
|
BRQ changeDirTake
|
|
SETD.2 SbfsFoundFlags
|
|
LDA.2
|
|
INIB 0x02
|
|
AND
|
|
BRQ changeDirNo
|
|
|
|
changeDirTake:
|
|
CALL fileForget ; A relative path means something else from here.
|
|
SETD.0 SbfsAt
|
|
SETD.1 SbfsCwd
|
|
CALL sbfsCopyWord
|
|
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
changeDirNo:
|
|
INIA 0d1
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; ---- cd ----
|
|
;
|
|
; Moves the machine. What changes is two bytes, because the working directory is an entry
|
|
; index and nothing else: no path is stored anywhere, and the one on the prompt is worked
|
|
; out again each time from the chain of parents.
|
|
;
|
|
; "cd" on its own goes to the root, which is the only place that is always there and the
|
|
; only sensible thing to mean by home on a machine with no idea who is using it.
|
|
doCd:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA fileNoDisk
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA cdRoot
|
|
|
|
CALL sbfsWalk
|
|
BNQ cdNoSuch
|
|
|
|
; It has to be a directory to stand in. Ending at the root is fine and is the one case
|
|
; with no entry to ask, since the root is not an entry.
|
|
SETD.0 SbfsAt
|
|
LDA.0
|
|
INCD.0
|
|
LDB.0
|
|
OR
|
|
BRQ cdTake
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x02
|
|
AND
|
|
BRQ cdNotDirectory
|
|
|
|
cdTake:
|
|
; WHAT A RELATIVE PATH MEANS HAS JUST CHANGED, so the remembered file goes. It is keyed
|
|
; on the path as somebody typed it, and "notes.txt" is a different file from here than
|
|
; it was a moment ago. Nothing about the entry it remembers has changed, which is what
|
|
; makes this the kind of stale that is believed rather than noticed.
|
|
CALL fileForget
|
|
|
|
SETD.0 SbfsAt
|
|
SETD.1 SbfsCwd
|
|
CALL sbfsCopyWord
|
|
BRI prompt
|
|
|
|
cdRoot:
|
|
CALL fileForget
|
|
SETD.0 SbfsCwd
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
BRI prompt
|
|
|
|
cdNoSuch:
|
|
SETD.0 NoSuchFile
|
|
BRI fileComplain
|
|
cdNotDirectory:
|
|
SETD.0 NotDirectory
|
|
BRI fileComplain
|
|
|
|
; ---- mkdir and rmdir ----
|
|
;
|
|
; Two commands rather than one that works out what you meant, and delete stays for files
|
|
; only. Each of the four says exactly what it will take, so none of them can be the one
|
|
; that took away more than was asked for.
|
|
doMkdir:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA fileNoDisk
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA mkdirWhat
|
|
|
|
CALL sbfsMakeDir
|
|
BNQ mkdirFailed
|
|
SETD.0 MadeText
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
mkdirWhat:
|
|
SETD.0 MkdirWhat
|
|
BRI fileComplain
|
|
mkdirFailed:
|
|
SETD.0 MkdirNo
|
|
BRI fileComplain
|
|
|
|
doRmdir:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA fileNoDisk
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA rmdirWhat
|
|
|
|
; What a name means on the disk is about to change, so the remembered file goes.
|
|
CALL fileForget
|
|
CALL sbfsRemoveDir
|
|
BNQ rmdirFailed
|
|
SETD.0 RemovedText
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
rmdirWhat:
|
|
SETD.0 RmdirWhat
|
|
BRI fileComplain
|
|
rmdirFailed:
|
|
SETD.0 RmdirNo
|
|
BRI fileComplain
|
|
|
|
; ---- Writing out where the machine is ----
|
|
;
|
|
; Builds the working directory's path into CwdText and leaves CwdAt pointing at where it
|
|
; begins. Nothing stores a path, so this walks up the chain of parents.
|
|
;
|
|
; WRITTEN BACKWARDS, from the end of the buffer towards the front, because that is the
|
|
; order the names arrive in and reversing them afterwards would want somewhere to put them
|
|
; in the meantime. What comes back is a pointer into the middle of the buffer rather than
|
|
; to the front of it, which costs nothing to print from.
|
|
shellPath:
|
|
; The end of the buffer, holding the zero that ends the string.
|
|
SETD.0 CwdText
|
|
DPUP.0 0d126
|
|
RSTA
|
|
STA.0
|
|
SETD.1 CwdAt
|
|
STD.0.1
|
|
|
|
; HOW MUCH THERE IS TO WRITE INTO, COUNTED DOWN. Nothing bounds how deep the directories
|
|
; go: a path is capped at what one operation can name, but "mkdir a" and "cd a" are each
|
|
; well inside that and can be typed all day. This walk writes BACKWARDS from the end, so
|
|
; running out means walking off the FRONT of the buffer and into whatever the assembler
|
|
; put below it - which was the shell's own command names. Six directories of twenty two
|
|
; characters was enough, and the first five bytes to go were the word "exit", so the
|
|
; shell stopped recognising the command for leaving.
|
|
;
|
|
; A hundred and twenty three of the hundred and twenty six, the other three being kept
|
|
; back for the dots that say it was cut.
|
|
INIA 0d123
|
|
SETD.0 CwdRoom
|
|
STA.0
|
|
RSTA
|
|
SETD.0 CwdCut
|
|
STA.0
|
|
|
|
; Where the walk up starts.
|
|
SETD.0 SbfsCwd
|
|
SETD.1 CwdWalk
|
|
CALL sbfsCopyWord
|
|
|
|
shellPathStep:
|
|
SETD.0 CwdWalk
|
|
LDA.0
|
|
INCD.0
|
|
LDB.0
|
|
OR
|
|
BRQ shellPathDone ; The root, which is where every path begins.
|
|
|
|
SETD.0 CwdWalk
|
|
SETD.1 SbfsTarget
|
|
CALL sbfsCopyWord
|
|
SETD.0 SbfsTarget
|
|
CALL sbfsBackWord
|
|
CALL sbfsAtIndex
|
|
BNQ shellPathDone ; The disk would not read, so say as much as is known.
|
|
|
|
; The name goes in front of what is there, and a separator in front of that.
|
|
SETD.0 SbfsName
|
|
CALL shellPathPrepend
|
|
SETD.0 CwdCut
|
|
LDA.0
|
|
BNA shellPathReady ; It would not all fit, so there is nothing above worth asking for.
|
|
|
|
SETD.0 SbfsUpParent
|
|
SETD.1 CwdWalk
|
|
CALL sbfsCopyWord
|
|
BRI shellPathStep
|
|
|
|
shellPathDone:
|
|
; A machine at the root has written nothing at all, and the path to the root is the
|
|
; separator on its own.
|
|
SETD.0 CwdAt
|
|
LDD.1.0
|
|
LDA.1
|
|
BNA shellPathReady
|
|
SETD.0 Separator
|
|
CALL shellPathPrepend
|
|
shellPathReady:
|
|
RET
|
|
|
|
; DP0 names a string. Puts it in front of what CwdAt points at, with a separator before
|
|
; it, and moves CwdAt back over the lot.
|
|
;
|
|
; The string has to be measured before it can be written, since it is written from its
|
|
; last character backwards. Nothing here is long enough for that to be worth avoiding.
|
|
shellPathPrepend:
|
|
SETD.1 PathLength
|
|
RSTA
|
|
STA.1
|
|
pathMeasure:
|
|
LDA.0
|
|
BRA pathMeasured
|
|
SETD.1 PathLength
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
INCD.0
|
|
BRI pathMeasure
|
|
pathMeasured:
|
|
; DP0 is on the zero at the end. Step back onto the last character, unless there is not
|
|
; one, in which case only the separator goes in.
|
|
SETD.1 CwdAt
|
|
LDD.2.1 ; DP2 is where the string already begins.
|
|
|
|
pathBack:
|
|
SETD.1 PathLength
|
|
LDA.1
|
|
BRA pathSeparator
|
|
DECA
|
|
STA.1
|
|
|
|
SETD.1 CwdRoom
|
|
LDA.1
|
|
BRA pathCut ; The front of the buffer, and the next byte would be past it.
|
|
DECA
|
|
STA.1
|
|
|
|
DECD.0
|
|
DECD.2
|
|
LDA.0
|
|
STA.2
|
|
BRI pathBack
|
|
|
|
pathSeparator:
|
|
SETD.1 CwdRoom
|
|
LDA.1
|
|
BRA pathCut
|
|
DECA
|
|
STA.1
|
|
|
|
DECD.2
|
|
INIA 0x2F
|
|
STA.2
|
|
SETD.1 CwdAt
|
|
STD.2.1
|
|
RET
|
|
|
|
; Stopping rather than writing past the front. The names go on backwards, so what is
|
|
; already down is the DEEP end of the path - which is the end worth showing: the prompt
|
|
; says where you are, and the last two directories say that better than the first two do.
|
|
;
|
|
; Three dots in front, out of the room that was never counted, so there is always
|
|
; somewhere to put them. No separator: the name they sit against brought its own, or was
|
|
; cut off half way, and "..." reads correctly against either.
|
|
pathCut:
|
|
INIA 0x2E
|
|
DECD.2
|
|
STA.2
|
|
DECD.2
|
|
STA.2
|
|
DECD.2
|
|
STA.2
|
|
SETD.1 CwdAt
|
|
STD.2.1
|
|
INIA 0x01
|
|
SETD.1 CwdCut
|
|
STA.1
|
|
RET
|
|
|
|
; ---- delete and rename ----
|
|
;
|
|
; The two things a disk needs that reading and writing do not provide, and the two that
|
|
; anything editing a document will want from the shell as well as from a program. Deleting
|
|
; frees an entry and its blocks; renaming changes twenty two bytes and moves nothing.
|
|
|
|
doDelete:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA fileNoDisk
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA deleteWhat
|
|
|
|
; What a name means on the disk is about to change, so the remembered file goes.
|
|
CALL fileForget
|
|
CALL sbfsDelete
|
|
BNQ deleteFailed
|
|
SETD.0 Deleted
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
deleteWhat:
|
|
SETD.0 DeleteWhat
|
|
BRI fileComplain
|
|
|
|
deleteFailed:
|
|
; Two refusals arrive here as one. Asking again costs a walk of the directory, which is
|
|
; nothing on a path nobody takes twice, and it is the difference between "you typed a
|
|
; name that is not there" and "that is a directory, and delete does not take those".
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL sbfsFind
|
|
BNQ deleteNoSuch
|
|
SETD.0 SbfsFoundFlags
|
|
LDA.0
|
|
INIB 0x02
|
|
AND
|
|
BNQ deleteIsDirectory
|
|
deleteNoSuch:
|
|
SETD.0 NoSuchFile
|
|
BRI fileComplain
|
|
deleteIsDirectory:
|
|
SETD.0 IsDirectory
|
|
BRI fileComplain
|
|
|
|
doRename:
|
|
SETD.0 DiskReady
|
|
LDA.0
|
|
BRA fileNoDisk
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA renameWhat
|
|
|
|
; Two names, so the rest of the line is split again. textSplit writes a zero over the
|
|
; space it cuts at, so what was one string becomes two without anything being copied.
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
SETD.1 RenameFrom
|
|
STD.0.1
|
|
CALL textSplit
|
|
|
|
SETD.1 TextRest
|
|
LDD.1.1
|
|
LDA.1
|
|
BRA renameWhat ; Only one name was given, and this needs both.
|
|
|
|
SETD.2 RenameFrom
|
|
LDD.0.2
|
|
; What a name means on the disk is about to change, so the remembered file goes.
|
|
CALL fileForget
|
|
CALL sbfsRename
|
|
BNQ renameFailed
|
|
SETD.0 Renamed
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
renameWhat:
|
|
SETD.0 RenameWhat
|
|
BRI fileComplain
|
|
renameFailed:
|
|
; Either there is no such file or the new name is already taken. Which of the two is not
|
|
; worth another message: both mean the disk does not have room for that name to move.
|
|
SETD.0 RenameNo
|
|
BRI fileComplain
|
|
|
|
fileNoDisk:
|
|
SETD.0 NoDisk
|
|
fileComplain:
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; ---- drive ----
|
|
;
|
|
; Which disk the shell is standing on. With nothing after it, says which; with a number, goes
|
|
; there - and the working directory goes with it, because where you are on a disk is part of
|
|
; which disk you are on rather than something the shell keeps on the side.
|
|
doDrive:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA driveSay
|
|
|
|
; One digit. Anything else is not a drive number, and the machines this is imitating never
|
|
; had ten drives either.
|
|
INIB 0d48
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
INIB 0d10
|
|
CCF
|
|
SUB
|
|
BNC driveNoSuch ; Ten or more, so it was not a digit at all.
|
|
|
|
; Is there such a drive on this machine?
|
|
SETD.1 DriveWanted
|
|
STA.1
|
|
INB 0x25 ; How many drives, straight into B: there is no move from A to it.
|
|
LDA.1
|
|
CCF
|
|
SUB ; Borrows when the wanted one is inside the count.
|
|
BNC driveNoSuch
|
|
|
|
; And is there anything readable in it? A drive with no disk is a real drive and an empty
|
|
; one, so this is a different answer from "there is no such drive".
|
|
CALL sbfsDriveBit
|
|
MVQA
|
|
SETD.1 SbfsMounted
|
|
LDB.1
|
|
AND
|
|
BRQ driveNotReadable
|
|
|
|
SETD.1 DriveWanted
|
|
LDA.1
|
|
CALL sbfsUse
|
|
BRI prompt
|
|
|
|
driveSay:
|
|
INA 0x24
|
|
INIB 0d48
|
|
CCF
|
|
ADD
|
|
OUTQ 0x00
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
driveNoSuch:
|
|
SETD.0 DriveNoSuch
|
|
BRI fileComplain
|
|
driveNotReadable:
|
|
SETD.0 DriveNotReadable
|
|
BRI fileComplain
|
|
|
|
; ---- clear ----
|
|
;
|
|
; The console has done this since before there was a screen to do it on: writing 1 to the
|
|
; command port. On a terminal it is what a terminal does with it, and on the Voyager it is
|
|
; the screen going blank and the cursor going home.
|
|
doClear:
|
|
INIA 0x01
|
|
OUTA 0x05
|
|
BRI prompt
|
|
|
|
; ---- echo ----
|
|
;
|
|
; Says the rest of the line and nothing else. Say.sbx has done this since before there were
|
|
; scripts, and is the wrong shape for one: it is a program, so it has to be found on the
|
|
; disk and loaded and started, it prefixes what it was told with "it says:", and the system
|
|
; prints "finished" after it. That is three lines of noise around one line of narration.
|
|
;
|
|
; A script telling you what it is doing is the ordinary case now, so it costs a command
|
|
; rather than a program. With nothing after it, a blank line - which is what anybody will
|
|
; expect and is worth having for spacing a long script out.
|
|
doEcho:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; ---- do ----
|
|
;
|
|
; Runs the lines in a file as though they had been typed. What makes a file one of these is
|
|
; the #! on the front of it, not its name and not a flag in its directory entry: the rule is
|
|
; that the entry holds only what the content cannot say about itself, and a script can say
|
|
; what it is. The loader already refuses anything that is not SBEX, so the two kinds of
|
|
; runnable thing turn each other away without either of them knowing about the other.
|
|
doScript:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA scriptNoName
|
|
|
|
CALL scriptOpen
|
|
BRQ prompt ; It is open, and the next line read will come from it.
|
|
|
|
; Which of the two went wrong. A number would be no use to anybody here.
|
|
MVQA
|
|
INIB 0x01
|
|
CCF
|
|
SUB
|
|
BRQ scriptNoFile
|
|
INIB 0x02
|
|
CCF
|
|
SUB
|
|
BRQ scriptNotOne
|
|
SETD.0 ScriptTooDeep
|
|
BRI fileComplain
|
|
scriptNotOne:
|
|
SETD.0 ScriptNotOne
|
|
BRI fileComplain
|
|
scriptNoFile:
|
|
SETD.0 ScriptNoFile
|
|
BRI fileComplain
|
|
scriptNoName:
|
|
SETD.0 ScriptUsage
|
|
BRI fileComplain
|
|
|
|
; Where the person was standing when a program was started, so it can be given back when the
|
|
; program has finished with the machine. A program that copies between two disks moves the
|
|
; drive as its own paths need it to; that is its business, and being left on the disk it
|
|
; happened to finish with is not what the person asked for.
|
|
;
|
|
; SET WHEREVER A PROGRAM STARTS, of which there are two: run, and typing a program's name.
|
|
; Restored in handleExit, which is the one place they both come back through.
|
|
; ---- Putting the screen somewhere and getting it back ----
|
|
;
|
|
; A program that takes the whole screen leaves the shell a blank one, and everything that was
|
|
; on it - the listing you were reading, the error you were about to act on - is gone. There is
|
|
; nowhere to put 32K on a machine with 64K of Data Memory that CosmOS is already living in.
|
|
;
|
|
; A DRIVE MADE OF MEMORY IS SOMEWHERE. The map goes to a file on the scratch drive like any
|
|
; other file, and comes back the same way; the filesystem does the allocating, and this had to
|
|
; invent nothing at all to have somewhere to put it.
|
|
;
|
|
; EVERYTHING A PROGRAM CAN DISTURB, which is more than the part on screen. The map's off
|
|
; screen rows are the console's scrollback; the tiles are the font, which a program that
|
|
; redefines one has overwritten; and the palette is where the console's own colours live -
|
|
; Grid could give back the map and not the colours, and handed the shell green text on blue.
|
|
;
|
|
; 192 pages of tiles and map, which are next to each other, then the four of palette. 196 in
|
|
; all, and a register block on the front.
|
|
;
|
|
; ---- Saved on being asked, restored on the way out ----
|
|
;
|
|
; Saving on every program start would be cheap enough. Restoring on every exit would be
|
|
; WRONG: dir, Files and Say print and stop, and their output is the reason you ran them.
|
|
; So a program says it is taking the screen, and one that says nothing behaves exactly as
|
|
; every program did before this existed.
|
|
screenTake:
|
|
SETD.0 SbfsScratch1
|
|
LDA.0
|
|
INIB 0xFF
|
|
CCF
|
|
SUB
|
|
BRQ screenNoScratch ; No volatile drive, so nowhere to put it.
|
|
|
|
; Where the caller was, and where the file is going.
|
|
INA 0x24
|
|
SETD.0 ScreenWasDrive
|
|
STA.0
|
|
SETD.0 SbfsScratch1
|
|
LDA.0
|
|
CALL sbfsUse
|
|
|
|
CALL screenBank
|
|
|
|
; A hundred and twenty nine blocks: one of registers, then the map.
|
|
SETD.0 SbfsFileBlocks
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
INIA 0d197
|
|
STA.0
|
|
SETD.0 SbfsFileTail
|
|
RSTA
|
|
STA.0
|
|
SETD.0 ScreenFileName
|
|
CALL sbfsStreamStart
|
|
BNQ screenTakeFailed
|
|
|
|
; Block nought is where the screen was, rather than what was on it: the cursor, the four
|
|
; scroll registers and the mode. A picture put back under a different origin is not the
|
|
; picture that was taken.
|
|
CALL screenClearBlock
|
|
SETD.0 ScreenBlock
|
|
INA 0x03
|
|
STA.0
|
|
INCD.0
|
|
INA 0x04
|
|
STA.0
|
|
INCD.0
|
|
INA 0x34
|
|
STA.0
|
|
INCD.0
|
|
INA 0x36
|
|
STA.0
|
|
INCD.0
|
|
INA 0x37
|
|
STA.0
|
|
INCD.0
|
|
INA 0x38
|
|
STA.0
|
|
INCD.0
|
|
INA 0x31
|
|
STA.0
|
|
|
|
RSTA
|
|
RSTB
|
|
SETD.2 SbfsIndex
|
|
STA.2
|
|
INCD.2
|
|
STB.2
|
|
SETD.1 ScreenBlock
|
|
CALL sbfsStreamWrite
|
|
BNQ screenTakeFailed
|
|
|
|
; And the map, a block at a time through the one buffer there is.
|
|
RSTA
|
|
SETD.0 ScreenAt
|
|
STA.0
|
|
screenTakeBlock:
|
|
SETD.0 ScreenAt
|
|
LDA.0
|
|
CALL screenPageFor
|
|
MVQA
|
|
CALL screenFromVideo
|
|
|
|
SETD.0 ScreenAt
|
|
LDA.0
|
|
INCA
|
|
SETD.2 SbfsIndex
|
|
RSTB
|
|
STB.2
|
|
INCD.2
|
|
STA.2 ; Block n of the map is block n+1 of the file.
|
|
SETD.1 ScreenBlock
|
|
CALL sbfsStreamWrite
|
|
BNQ screenTakeFailed
|
|
|
|
SETD.0 ScreenAt
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
INIB 0d196
|
|
CCF
|
|
SUB
|
|
BNQ screenTakeBlock
|
|
|
|
SETD.0 SbfsFileBlocks
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
INIA 0d197
|
|
STA.0
|
|
SETD.0 SbfsFileTail
|
|
RSTA
|
|
STA.0
|
|
CALL sbfsStreamDone
|
|
BNQ screenTakeFailed
|
|
|
|
INIA 0x01
|
|
SETD.0 ScreenSaved
|
|
STA.0
|
|
CALL screenGoBack
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
screenTakeFailed:
|
|
RSTA
|
|
SETD.0 ScreenSaved
|
|
STA.0
|
|
CALL screenGoBack
|
|
screenNoScratch:
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Whatever screenTake put away, put back. Nothing at all if it never ran.
|
|
screenGive:
|
|
SETD.0 ScreenSaved
|
|
LDA.0
|
|
BRA screenGiveNone
|
|
|
|
RSTA
|
|
STA.0 ; Once only: the next program takes its own.
|
|
INA 0x24
|
|
SETD.0 ScreenWasDrive
|
|
STA.0
|
|
SETD.0 SbfsScratch1
|
|
LDA.0
|
|
CALL sbfsUse
|
|
|
|
CALL screenBank
|
|
|
|
SETD.0 ScreenFileName
|
|
CALL fileLookup
|
|
BNQ screenGiveDone
|
|
|
|
; The map first, then the registers, so that nothing is drawn under an origin that is about
|
|
; to change.
|
|
RSTA
|
|
SETD.0 ScreenAt
|
|
STA.0
|
|
screenGiveBlock:
|
|
SETD.0 ScreenAt
|
|
LDA.0
|
|
INCA
|
|
SETD.2 SbfsIndex
|
|
RSTB
|
|
STB.2
|
|
INCD.2
|
|
STA.2
|
|
SETD.1 ScreenBlock
|
|
CALL sbfsReadOne
|
|
BNQ screenGiveDone
|
|
|
|
SETD.0 ScreenAt
|
|
LDA.0
|
|
CALL screenPageFor
|
|
MVQA
|
|
CALL screenToVideo
|
|
|
|
SETD.0 ScreenAt
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
INIB 0d196
|
|
CCF
|
|
SUB
|
|
BNQ screenGiveBlock
|
|
|
|
; And where it was.
|
|
RSTA
|
|
RSTB
|
|
SETD.2 SbfsIndex
|
|
STA.2
|
|
INCD.2
|
|
STB.2
|
|
SETD.1 ScreenBlock
|
|
CALL sbfsReadOne
|
|
BNQ screenGiveDone
|
|
|
|
SETD.0 ScreenBlock
|
|
LDA.0
|
|
OUTA 0x03
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x04
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x34
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x36
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x37
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x38
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0x31
|
|
|
|
screenGiveDone:
|
|
CALL screenGoBack
|
|
screenGiveNone:
|
|
RET
|
|
|
|
; The registers a console cannot work without, put back. Not the picture - that is
|
|
; screenGive's, and only happens for a program that asked.
|
|
screenSane:
|
|
RSTA
|
|
OUTA 0x37
|
|
OUTA 0x38 ; No fraction of a cell in either direction.
|
|
OUTA 0x3C ; And the screen everybody else's map is in.
|
|
|
|
; ---- Which of the two screens is being shown ----
|
|
;
|
|
; A program that draws into the back buffer and flips is showing screen one, and the shell
|
|
; knows nothing about that. Its own scrollback, its prompt and every line the person typed
|
|
; are in screen NOUGHT, so a program that exited while flipped would hand back a shell
|
|
; drawing correctly onto a screen nobody had ever written to - blank, and looking for all
|
|
; the world like the machine had lost everything.
|
|
;
|
|
; Put back rather than left, by the same rule as the cursor and the ink in handleExit: a
|
|
; program that stopped early is not around to put anything back, and the shell owns what
|
|
; the person is looking at.
|
|
|
|
; ---- And glyphs and colours that can be read ----
|
|
;
|
|
; A program may redefine any tile and any colour, and the font lives in tiles like anything
|
|
; else - so a program that redefined a letter has left the shell unable to spell, and one
|
|
; that wrote its own palette handed back green text on blue. Both used to be permanent:
|
|
; there was nowhere to get the originals from, because the only copy was the one that had
|
|
; been drawn over. The device has a character generator now and this is what it is for.
|
|
;
|
|
; BEFORE screenGive, like everything else here, so that a program which SAVED the screen
|
|
; gets back what was actually on it rather than what a clean machine looks like. This is
|
|
; the floor for the programs that saved nothing.
|
|
INIA 0x03
|
|
OUTA 0x39
|
|
|
|
; ---- And no sprites left standing over the shell ----
|
|
;
|
|
; The table is in the atlas at 0xC000, and the screen save does not cover it: the pages it
|
|
; walks run to the end of the map and pick up again at the palette, with the sprites in the
|
|
; gap between. That is deliberate rather than an oversight. Nothing the shell draws is a
|
|
; sprite, so there is nothing to GIVE BACK - what is needed is to take away, or a program
|
|
; that put something on the screen and left would have left it sitting over the prompt,
|
|
; still there, still in front of everything, and nothing able to type it away.
|
|
;
|
|
; A size of nought is no sprite, so filling the table with nought is all of it, and it is
|
|
; one command rather than 256 of them.
|
|
CALL screenBank
|
|
INIA 0d4
|
|
OUTA 0xE3
|
|
INIA 0xC0
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
|
|
INIA 0x08
|
|
OUTA 0xE6
|
|
RSTA
|
|
OUTA 0xE7 ; 0x0800, which is 256 entries of eight bytes.
|
|
INIA 0x02
|
|
OUTA 0xE8
|
|
RET
|
|
|
|
; Video memory as banks 4 and 5, which is what makes it reachable at all. Bank 3 is the
|
|
; disk's; see the table in the CosmOS README, which exists because a program once took 3.
|
|
;
|
|
; The BACK BUFFER is not given a number here. The system saves and restores what is on the
|
|
; screen, and what is on the screen is screen nought - a program that wants the other one
|
|
; registers it itself, the same as any program wanting memory nobody else is using.
|
|
screenBank:
|
|
INIA 0d4
|
|
OUTA 0xE3
|
|
INIA 0x30
|
|
OUTA 0xE2
|
|
INIA 0x03
|
|
OUTA 0xE8 ; Four is the atlas: the tiles and the palette.
|
|
INIA 0d5
|
|
OUTA 0xE3
|
|
INIA 0x3A
|
|
OUTA 0xE2
|
|
INIA 0x03
|
|
OUTA 0xE8 ; Five is the screen: the map, or a bitmap.
|
|
RET
|
|
|
|
; Back to the drive whoever called was standing on.
|
|
screenGoBack:
|
|
SETD.0 ScreenWasDrive
|
|
LDA.0
|
|
CALL sbfsUse
|
|
RET
|
|
|
|
; ---- Which page of video memory a saved block is ----
|
|
;
|
|
; Nought to 191 are the tiles and the map, which sit next to each other from 0x0000. After
|
|
; that comes 16K of nothing, so 192 to 195 jump to 0xFC and are the palette. One sum rather
|
|
; than two loops, because two loops is two places to get the file's block numbers wrong.
|
|
screenPageFor:
|
|
INIB 0d192
|
|
CCF
|
|
SUB
|
|
BRC screenPageDirect ; Borrowed, so it is below 192 and the page is the index.
|
|
MVQA
|
|
INIB 0xFC
|
|
CCF
|
|
ADD
|
|
RET
|
|
screenPageDirect:
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- And which of the screen's two banks that page is in ----
|
|
;
|
|
; The tiles and the palette are the atlas and the map is the screen, so the PAGE NUMBER
|
|
; ALREADY SAYS WHICH - 0x00 to 0x3F and 0xFC to 0xFF one way, 0x40 to 0xBF the other. Nothing
|
|
; has to be remembered alongside the page, and there is no second list to keep in step with
|
|
; screenPageFor above.
|
|
;
|
|
; IN Q AND NOT A, for the same reason screenPageFor answers there: RET puts A back the way it
|
|
; found it, so a subroutine that answered in A would answer with the argument it was given.
|
|
screenBankFor:
|
|
INIB 0x40
|
|
CCF
|
|
SUB
|
|
BRC screenBankAtlas ; Borrowed, so it is below 0x40 and is a tile.
|
|
INIB 0xC0
|
|
CCF
|
|
SUB
|
|
BRC screenBankScreen ; Borrowed, so it is below 0xC0 and is the map.
|
|
screenBankAtlas:
|
|
INIA 0d4
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
screenBankScreen:
|
|
INIA 0d5
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; A holds a page of video memory. Its 256 bytes come into ScreenBlock.
|
|
screenFromVideo:
|
|
CALL screenBankFor ; A is the page and RET hands it back; Q comes back the bank.
|
|
PSHA ; The page, which the bank is about to want A for.
|
|
MVQA
|
|
OUTA 0xE0
|
|
POPA
|
|
OUTA 0xE1
|
|
RSTA
|
|
OUTA 0xE2
|
|
INIA 0x01
|
|
OUTA 0xE3 ; Into Data Memory.
|
|
CALL screenBufferDest
|
|
CALL screenLength
|
|
INIA 0x01
|
|
OUTA 0xE8
|
|
RET
|
|
|
|
; A holds a page of video memory. ScreenBlock goes back into it.
|
|
screenToVideo:
|
|
CALL screenBankFor
|
|
PSHA
|
|
MVQA
|
|
OUTA 0xE3
|
|
POPA
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
INIA 0x01
|
|
OUTA 0xE0 ; Out of Data Memory.
|
|
CALL screenBufferSource
|
|
CALL screenLength
|
|
INIA 0x01
|
|
OUTA 0xE8
|
|
RET
|
|
|
|
; Where ScreenBlock is, told to the controller. A Data Pointer's two bytes cannot be read out
|
|
; of it, so it goes to memory first and comes back a byte at a time.
|
|
screenBufferDest:
|
|
SETD.1 ScreenBlockAt
|
|
SETD.0 ScreenBlock
|
|
STD.0.1
|
|
LDA.1
|
|
OUTA 0xE4
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE5
|
|
RET
|
|
|
|
screenBufferSource:
|
|
SETD.1 ScreenBlockAt
|
|
SETD.0 ScreenBlock
|
|
STD.0.1
|
|
LDA.1
|
|
OUTA 0xE1
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE2
|
|
RET
|
|
|
|
screenLength:
|
|
INIA 0x01
|
|
OUTA 0xE6
|
|
RSTA
|
|
OUTA 0xE7 ; 0x0100, which is one block.
|
|
RET
|
|
|
|
; The block, emptied. Everything the register block below does not fill has to be nought, or
|
|
; a screen restored would carry whatever the last file read left in here.
|
|
screenClearBlock:
|
|
SETD.0 ScreenBlock
|
|
RSTB
|
|
screenClearByte:
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BNB screenClearByte
|
|
RET
|
|
|
|
; ---- run ----
|
|
;
|
|
; Hands the machine to whatever was loaded. Where the Stack is now is written down first,
|
|
; because the program is not going to unwind anything it pushes and the exit handler has
|
|
; to be able to put the Stack back.
|
|
|
|
doRun:
|
|
SETD.0 LoadedOk
|
|
LDA.0
|
|
BRA runNothing
|
|
|
|
; Where typing a program's name arrives, having loaded it on the way. The argument comes
|
|
; out of TextRest either way: after "run" that is what followed the word, and after a
|
|
; program's own name it is what followed the name, which is the same thing meaning the
|
|
; same thing.
|
|
runLoaded:
|
|
INA 0x24
|
|
SETD.0 RunDrive
|
|
STA.0
|
|
MVSD.0
|
|
SETD.1 SystemStack
|
|
STD.0.1
|
|
|
|
; And where the machine is, so that the exit handler has something to put back.
|
|
SETD.0 SbfsCwd
|
|
SETD.1 SavedCwd
|
|
CALL sbfsCopyWord
|
|
|
|
; Whatever followed the word "run" is kept where the program can ask for it. Copied
|
|
; rather than pointed at, because what it is pointing at is the line the shell typed
|
|
; into, and a program is entitled to outlive the shell's opinion of that.
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
SETD.1 RunArgument
|
|
INIB 0d64
|
|
CALL copyText
|
|
|
|
CALL installVectors
|
|
|
|
; The entry address is a number until BRD makes it a place. DP3 is the one to build it
|
|
; in, because it is the pointer nothing puts back.
|
|
SETD.1 LoadedEntry
|
|
LDD.3.1
|
|
BRD.3
|
|
|
|
runNothing:
|
|
SETD.0 NothingLoaded
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; DP0 is a string, DP1 is where it should go, and B is how much room there is counting
|
|
; the zero on the end. What does not fit is left behind, and what is written is a string
|
|
; either way.
|
|
copyText:
|
|
BRB copyTextDone ; No room at all, so nothing is written, not even the zero.
|
|
copyTextLoop:
|
|
DECB
|
|
BRB copyTextEnd ; Only room for the terminator now.
|
|
LDA.0
|
|
STA.1
|
|
BRA copyTextDone
|
|
INCD.0
|
|
INCD.1
|
|
BRI copyTextLoop
|
|
copyTextEnd:
|
|
RSTA
|
|
STA.1
|
|
copyTextDone:
|
|
RET
|
|
|
|
; ---- Putting a program's vectors in, and taking them out again ----
|
|
;
|
|
; The vector table lives in Program Memory, which no instruction can write, so both of
|
|
; these go through the memory controller. Port 0xE9 reads a byte from the source and writes
|
|
; a byte to the destination, stepping the address on either way, so a two byte entry is two
|
|
; reads or two writes and no address arithmetic in between.
|
|
;
|
|
; What was in the slot is kept before anything replaces it, and put back afterwards, rather
|
|
; than the slot being cleared. Clearing would be wrong wherever a program has installed a
|
|
; handler over one the system was already using: the program is allowed to do that, and
|
|
; when it goes, what it covered up has to come back rather than becoming a hole.
|
|
|
|
installVectors:
|
|
SETD.0 LoadedVectorCount
|
|
LDA.0
|
|
BRA installDone
|
|
SETD.1 VectorsLeft
|
|
STA.1
|
|
SETD.3 LoadedVectors
|
|
|
|
installOne:
|
|
; DP3 walks one six byte entry: where it goes, what goes there, and room for what was
|
|
; there before. Reading and writing the same slot, so the controller is pointed at it
|
|
; from both ends at once and the address is only worked out once.
|
|
RSTA
|
|
OUTA 0xE0 ; SourceBank: Program Memory.
|
|
OUTA 0xE3 ; DestBank: the same.
|
|
LDA.3
|
|
OUTA 0xE1
|
|
OUTA 0xE4
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE2
|
|
OUTA 0xE5
|
|
INCD.3 ; On the handler.
|
|
|
|
; What is there now, before anything replaces it.
|
|
INA 0xE9
|
|
PSHA
|
|
INA 0xE9
|
|
PSHA
|
|
|
|
; And the handler in its place.
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3 ; On the two bytes kept for what was there before.
|
|
|
|
; The Stack gives them back in the reverse of the order they went on, so the low byte
|
|
; arrives first and is written to the second of the two. Getting this the natural way
|
|
; round instead put the low byte where the high one goes and the high byte over the
|
|
; handler, which the first run of a program survives - the table is already written by
|
|
; then - and the second run does not.
|
|
POPA
|
|
INCD.3
|
|
STA.3
|
|
DECD.3
|
|
POPA
|
|
STA.3
|
|
INCD.3
|
|
INCD.3
|
|
|
|
SETD.1 VectorsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA installOne
|
|
installDone:
|
|
RET
|
|
|
|
removeVectors:
|
|
SETD.0 LoadedVectorCount
|
|
LDA.0
|
|
BRA removeDone
|
|
SETD.1 VectorsLeft
|
|
STA.1
|
|
SETD.3 LoadedVectors
|
|
|
|
removeOne:
|
|
RSTA
|
|
OUTA 0xE3 ; DestBank: Program Memory.
|
|
LDA.3
|
|
OUTA 0xE4
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE5
|
|
INCD.3
|
|
INCD.3
|
|
INCD.3 ; Past the handler, to what was underneath it.
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
LDA.3
|
|
OUTA 0xE9
|
|
INCD.3
|
|
|
|
SETD.1 VectorsLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA removeOne
|
|
removeDone:
|
|
RET
|
|
|
|
; ---- The services ----
|
|
;
|
|
; These are what a loaded program is allowed to ask for. The names and their numbers come
|
|
; from services.asm, which the programs include as well, so neither side writes a number
|
|
; down and the two cannot disagree about them.
|
|
;
|
|
; A handler arrives with the caller's registers exactly as they were: an interrupt frame
|
|
; is pushed, not cleared. So the pointer a program put in DP0 is still there to be used.
|
|
|
|
; The disk finishing, acknowledged and ignored.
|
|
;
|
|
; The system drives the disk by asking its status port and waiting, so it has no use for
|
|
; the line. But the disk raises one after every operation whether anybody wants it or not,
|
|
; and a line goes on waiting while the Interrupt Flag is down rather than being lost. The
|
|
; shell keeps the flag down, so the line from the last disk read was still standing when
|
|
; the first program to enable interrupts ran, and it arrived there - a fault, in a program
|
|
; that had never heard of the disk, blamed on the innocent instruction that let it through.
|
|
;
|
|
; Answering a line is what takes it down, so this is one instruction and that is the point.
|
|
diskDone:
|
|
RETI
|
|
|
|
handlePrintString:
|
|
CALL printString
|
|
RETI
|
|
|
|
handleReadLine:
|
|
CALL editLinePlain
|
|
|
|
; readLine works out how long the line was, and it is already in Q where readLine left
|
|
; it. SRET keeps Q and DP3 and puts everything else back, so the answer simply stands.
|
|
SRET
|
|
|
|
; What the program was asked to work on. DP0 says where to put it and B how much room
|
|
; there is, counting the zero on the end, which is the same bargain readLine offers.
|
|
;
|
|
; Being asked for rather than left at an agreed address is deliberate. The two sides of
|
|
; this already have to agree on a vector number and nothing else, and that number is
|
|
; written down once in services.asm; an address would be a second thing to agree about, in
|
|
; a memory map that is a convention rather than anything enforced.
|
|
handleArgument:
|
|
PSHD.0
|
|
POPD.1
|
|
SETD.0 RunArgument
|
|
CALL copyText
|
|
RETI
|
|
|
|
; ---- The disk, on a program's behalf ----
|
|
;
|
|
; A loaded program that wanted a file used to include the whole filesystem, so it carried a
|
|
; private copy of code the system already has running, and mounted a disk that was already
|
|
; mounted. These are that code, reachable through a number instead.
|
|
;
|
|
; Every one of them answers in Q, and the answer is written into the frame over the saved
|
|
; register, because RETI puts every register back and would otherwise throw it away. That
|
|
; has to be done here rather than in a routine of its own: the offsets are from where the
|
|
; Stack Pointer is, and a CALL moves it by ten.
|
|
;
|
|
; A machine with no disk answers no to all of them rather than going ahead and finding out,
|
|
; because sbfs on a disk that was never mounted is reading whatever bank 3 happens to be.
|
|
|
|
; DP0 names the file, DP1 says where to put it. Q is zero if it read, and DP3 comes back
|
|
; holding how many bytes there were.
|
|
handleFileRead:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA fileReadNo
|
|
|
|
CALL sbfsFind
|
|
BNQ fileReadNo
|
|
|
|
; A file of 256 blocks is 64K, which will not fit in Data Memory and will not fit in the
|
|
; pointer that says how long it is either. Refused, rather than read as much of as fits:
|
|
; a length that lies is worse than a file that will not open.
|
|
SETD.2 SbfsFileBlocks
|
|
LDA.2
|
|
BNA fileReadNo
|
|
|
|
CALL sbfsRead
|
|
BNQ fileReadNo
|
|
|
|
; How long it is: the block count is the high byte of that and the tail is the low one,
|
|
; which is how a size is put together everywhere on this disk.
|
|
SETD.2 SbfsFileBlocks
|
|
INCD.2
|
|
LDA.2
|
|
SETD.2 SbfsFileTail
|
|
LDB.2
|
|
|
|
; A two byte answer, and DP3 is where a routine hands one back. Built with the
|
|
; Stack rather than written into the frame: SRET leaves DP3 alone, so there is
|
|
; nothing to reach into. PSHA then PSHB puts the high byte above the low one,
|
|
; which is the order POPD reads them in.
|
|
PSHA
|
|
PSHB
|
|
POPD.3
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: it is there.
|
|
SRET
|
|
|
|
fileReadNo:
|
|
INIA 0d1
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; DP0 names the file, DP1 is the bytes, and A and B together are how many. Q is zero if it
|
|
; saved. Whether it was there before makes no difference, which is what saving means.
|
|
handleFileSave:
|
|
SETD.2 DiskReady
|
|
PSHA
|
|
LDA.2
|
|
BRA fileSaveNoDisk
|
|
POPA
|
|
|
|
; Blocks are the high half of the count and the tail is the low half.
|
|
SETD.2 SbfsFileBlocks
|
|
PSHA
|
|
RSTA
|
|
STA.2 ; A whole file's block count fits in a byte, so this is zero.
|
|
INCD.2
|
|
POPA
|
|
STA.2
|
|
SETD.2 SbfsFileTail
|
|
STB.2
|
|
|
|
; What a name means on the disk is about to change, so the remembered file goes.
|
|
CALL fileForget
|
|
CALL sbfsSaveFile
|
|
SRET
|
|
|
|
fileSaveNoDisk:
|
|
POPA
|
|
INIA 0d1
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; DP0 names it. Q is zero if it went.
|
|
handleFileDelete:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA serviceNoDisk
|
|
; What a name means on the disk is about to change, so the remembered file goes.
|
|
CALL fileForget
|
|
CALL sbfsDelete
|
|
SRET
|
|
|
|
; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved.
|
|
handleFileRename:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA serviceNoDisk
|
|
; What a name means on the disk is about to change, so the remembered file goes.
|
|
CALL fileForget
|
|
CALL sbfsRename
|
|
SRET
|
|
|
|
serviceNoDisk:
|
|
INIA 0d1
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; ---- Reading a file that will not fit ----
|
|
;
|
|
; A file bigger than Data Memory cannot be handed over whole, and CosmOS's own source is
|
|
; now that file, so these two are how anything reads one: ask how many blocks, then ask for
|
|
; each block in turn. There is no open and no close. A program that stops halfway through
|
|
; leaves nothing behind, because there was never anything to leave.
|
|
|
|
; Finds a file, or remembers that it already did. DP0 names it. Q is zero if it is there,
|
|
; and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it exactly the way
|
|
; sbfsFind leaves them - whether the search happened or not, which is the whole point.
|
|
fileLookup:
|
|
SETD.2 FileCacheValid
|
|
LDA.2
|
|
BRA fileLookupSearch
|
|
|
|
SETD.1 FileCacheName
|
|
CALL textSame
|
|
BNQ fileLookupSearch
|
|
|
|
; ---- The same file as last time, and possibly not the same disk ----
|
|
;
|
|
; The description has to be put back, because anything that went to the disk in between - a
|
|
; directory listing, a program being loaded - left its own answer in those three.
|
|
;
|
|
; AND THE DRIVE WITH IT. Skipping the walk skips the drive the path named, so a cache hit
|
|
; on a machine that had moved read the right block numbers off the wrong disk. Copying
|
|
; between two disks is exactly that: block 0 walks and goes to the source, the write goes to
|
|
; the destination, and block 1 hits this cache. It only showed on files of more than one
|
|
; block, because a file of one is never looked up twice.
|
|
SETD.0 FileCacheDrive
|
|
LDA.0
|
|
CALL sbfsUse
|
|
|
|
SETD.0 SbfsFileStart
|
|
SETD.2 FileCacheStart
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileBlocks
|
|
SETD.2 FileCacheBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 FileCacheTail
|
|
LDA.0
|
|
SETD.0 SbfsFileTail
|
|
STA.0
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: found.
|
|
RET
|
|
|
|
fileLookupSearch:
|
|
CALL sbfsFind
|
|
BNQ fileLookupMissing
|
|
|
|
; Remember it. DP0 still names the file: a CALL puts the pointers back, which is the one
|
|
; place that convention is a convenience rather than an obstacle.
|
|
;
|
|
; A WHOLE PATH IS KEPT, not the twenty two bytes a name has. Keeping twenty two of a
|
|
; longer path cannot hand back the wrong file - textSame wants both strings to end in
|
|
; the same place, so a cut down entry misses rather than matching something else - but
|
|
; it can never match either, so every path longer than a name would go to the disk every
|
|
; single time and the cache would quietly stop being one.
|
|
SETD.1 FileCacheName
|
|
INIB 0d64
|
|
CALL copyText
|
|
SETD.0 FileCacheStart
|
|
SETD.2 SbfsFileStart
|
|
CALL sbfsSetWord
|
|
SETD.0 FileCacheBlocks
|
|
SETD.2 SbfsFileBlocks
|
|
CALL sbfsSetWord
|
|
SETD.0 SbfsFileTail
|
|
LDA.0
|
|
SETD.0 FileCacheTail
|
|
STA.0
|
|
|
|
; ---- And which disk those block numbers are on ----
|
|
;
|
|
; A start block means nothing without it. The walk above went to whatever drive the path
|
|
; named, so the answer is the drive now.
|
|
INA 0x24
|
|
SETD.0 FileCacheDrive
|
|
STA.0
|
|
|
|
; Marked good last, so that a cache half filled is never a cache believed.
|
|
INIA 0d1
|
|
SETD.0 FileCacheValid
|
|
STA.0
|
|
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: found.
|
|
RET
|
|
|
|
fileLookupMissing:
|
|
RET ; Q is not zero, and sbfsFind is what made it so.
|
|
|
|
; Throws the remembered file away. Everything that can change what a name means on the disk
|
|
; calls this before it does: a file saved over may have moved, a deleted one is gone, and a
|
|
; renamed one answers to something else. A remembered start block that survived any of
|
|
; those is a pointer at whatever took its place.
|
|
;
|
|
; Q is deliberately untouched, so this can be dropped into a handler without disturbing the
|
|
; answer that handler is in the middle of working out.
|
|
fileForget:
|
|
RSTA
|
|
SETD.0 FileCacheValid
|
|
STA.0
|
|
RET
|
|
|
|
; DP0 names it. Q is zero if it is there, and DP3 comes back holding how many blocks it
|
|
; occupies, counting a part one on the end.
|
|
;
|
|
; Blocks, not bytes, and that is forced rather than chosen: a file on a sixteen megabyte
|
|
; disk is up to twenty four bits long, which does not fit in a pointer. Blocks do, and the
|
|
; bytes in the last one come back from osFileBlock when the reader gets there.
|
|
handleFileInfo:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA fileInfoNoDisk
|
|
|
|
CALL fileLookup
|
|
BNQ fileInfoMissing
|
|
|
|
CALL sbfsFileExtent
|
|
SETD.2 SbfsWantBlocks
|
|
LDA.2
|
|
INCD.2
|
|
LDB.2
|
|
|
|
; A two byte answer, and DP3 is where a routine hands one back. Built with the
|
|
; Stack rather than written into the frame: SRET leaves DP3 alone, so there is
|
|
; nothing to reach into. PSHA then PSHB puts the high byte above the low one,
|
|
; which is the order POPD reads them in.
|
|
PSHA
|
|
PSHB
|
|
POPD.3
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: it is there.
|
|
SRET
|
|
|
|
fileInfoNoDisk:
|
|
INIA 0d1
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
fileInfoMissing:
|
|
INIA 0d2
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; DP0 names it, DP1 says where to put it, and A and B together are which block, counting
|
|
; from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes
|
|
; belong to the file.
|
|
handleFileBlock:
|
|
; Which block, before anything else, because finding out whether there is a disk needs A
|
|
; and there is nowhere else the number is written down.
|
|
SETD.2 SbfsIndex
|
|
STA.2
|
|
INCD.2
|
|
STB.2
|
|
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA fileBlockNoDisk
|
|
|
|
CALL fileLookup
|
|
BNQ fileBlockMissing
|
|
|
|
; Running off the end is how a reader finds out it has finished, so it gets an answer of
|
|
; its own rather than being told the disk failed.
|
|
CALL sbfsFileExtent
|
|
SETD.0 SbfsIndex
|
|
SETD.2 SbfsWantBlocks
|
|
CALL sbfsCompareWord
|
|
BNC fileBlockPastEnd ; The Carry is set only when the index is the smaller.
|
|
|
|
CALL sbfsReadOne ; DP1 still says where. A CALL puts the pointers back.
|
|
BNQ fileBlockFailed
|
|
|
|
; How much of it is the file's. Every block but a short last one is a whole 256, and 256
|
|
; is why this answers in a pointer instead of a register.
|
|
SETD.2 SbfsFileTail
|
|
LDA.2
|
|
BRA fileBlockWhole ; Nothing partial on the end, so they are all whole.
|
|
SETD.0 SbfsIndex
|
|
SETD.2 SbfsFileBlocks
|
|
CALL sbfsCompareWord
|
|
BNQ fileBlockWhole ; Not the last one.
|
|
|
|
SETD.2 SbfsFileTail
|
|
LDB.2
|
|
RSTA
|
|
BRI fileBlockAnswer
|
|
|
|
fileBlockWhole:
|
|
INIA 0x01
|
|
RSTB
|
|
|
|
fileBlockAnswer:
|
|
; A two byte answer, and DP3 is where a routine hands one back. Built with the
|
|
; Stack rather than written into the frame: SRET leaves DP3 alone, so there is
|
|
; nothing to reach into. PSHA then PSHB puts the high byte above the low one,
|
|
; which is the order POPD reads them in.
|
|
PSHA
|
|
PSHB
|
|
POPD.3
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q is zero: it is there.
|
|
SRET
|
|
|
|
fileBlockNoDisk:
|
|
INIA 0d1
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
fileBlockMissing:
|
|
INIA 0d2
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
fileBlockPastEnd:
|
|
INIA 0d3
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
fileBlockFailed:
|
|
INIA 0d4
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; A breakpoint. Shows every register as the interrupted program had them, waits for a key,
|
|
; and returns as though nothing happened.
|
|
;
|
|
; EVERY VALUE COMES OUT OF THE FRAME, not out of the registers, because by the time this
|
|
; runs the registers belong to the handler. The frame is what the program had, and RETI is
|
|
; going to give it all back, so what is shown is what will be resumed with.
|
|
;
|
|
; DP3 holds the frame throughout. It survives a CALL, and console.asm promises not to
|
|
; disturb it, which is what lets the printing routines be used between one field and the
|
|
; next. The Stack Pointer comes back to the same place after a balanced call, so the frame
|
|
; stays where it was found.
|
|
;
|
|
; +1 Status +2 Q +3 A +4 B +5 DP3 +7 DP2 +9 DP1 +11 DP0 +13 where it resumes
|
|
handleBreak:
|
|
MVSD.3
|
|
|
|
; Where it broke, which is two before where it resumes: the SWI and the vector it names.
|
|
SETD.0 BreakText
|
|
CALL printString
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d13
|
|
LDA.0
|
|
PSHA ; The high half, while the low one is worked on.
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d2
|
|
CCF
|
|
SUB ; Two back from where it resumes: the SWI and the vector it names.
|
|
MVQA
|
|
POPB
|
|
BRC breakBorrowed ; It borrowed, so the high half comes down by one.
|
|
BRI breakAddress
|
|
breakBorrowed:
|
|
DECB
|
|
breakAddress:
|
|
PSHA ; low
|
|
PSHB ; high
|
|
POPA
|
|
CALL printByteHex
|
|
POPA
|
|
CALL printByteHex
|
|
CALL newLine
|
|
|
|
SETD.0 ARegText
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d3
|
|
CALL breakByte
|
|
SETD.0 BRegText
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d4
|
|
CALL breakByte
|
|
SETD.0 QRegText
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d2
|
|
CALL breakByte
|
|
SETD.0 SRegText
|
|
PSHD.3
|
|
POPD.1
|
|
INCD.1
|
|
CALL breakByte
|
|
|
|
; And which bits of it those are, since a debug dump that makes you look the number up is
|
|
; only half of one. Halt is not among them: the machine is plainly not halted.
|
|
PSHD.3
|
|
POPD.1
|
|
INCD.1
|
|
LDA.1
|
|
PSHA
|
|
INIB 0x01
|
|
AND
|
|
BRQ breakNoCarry
|
|
SETD.0 CarryText
|
|
CALL printString
|
|
breakNoCarry:
|
|
POPA
|
|
PSHA
|
|
INIB 0x02
|
|
AND
|
|
BRQ breakNoFault
|
|
SETD.0 FaultText
|
|
CALL printString
|
|
breakNoFault:
|
|
POPA
|
|
INIB 0x04
|
|
AND
|
|
BRQ breakNoInts
|
|
SETD.0 IntsText
|
|
CALL printString
|
|
breakNoInts:
|
|
CALL newLine
|
|
|
|
SETD.0 DP0Text
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d11
|
|
CALL breakWord
|
|
SETD.0 DP1Text
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d9
|
|
CALL breakWord
|
|
SETD.0 DP2Text
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d7
|
|
CALL breakWord
|
|
SETD.0 DP3Text
|
|
PSHD.3
|
|
POPD.1
|
|
DPUP.1 0d5
|
|
CALL breakWord
|
|
|
|
; The Stack Pointer is not in the frame, because the frame is where the Stack Pointer is.
|
|
; What the program had is fourteen bytes above this one, that being what entering an
|
|
; interrupt puts down.
|
|
SETD.0 SPText
|
|
CALL printString
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d14
|
|
PSHD.0
|
|
POPB
|
|
POPA
|
|
CALL printByteHex
|
|
PSHB
|
|
POPA
|
|
CALL printByteHex
|
|
CALL newLine
|
|
|
|
; Anything typed carries on. Reading the data port waits however the console is set, which
|
|
; is one key if the program asked for key mode and a whole line if it did not - and either
|
|
; way it is the program's own console being borrowed for a moment.
|
|
SETD.0 ResumeText
|
|
CALL printString
|
|
INA 0x00
|
|
CALL newLine
|
|
RETI
|
|
|
|
; DP0 names a field and DP1 points at it in the frame. The caller does the stepping, with
|
|
; DPUP and a number written into the program, because a routine cannot hand a pointer back:
|
|
; CALL saves DP0 to DP2 and RET puts them back, so a walk done in here would be undone on
|
|
; the way out. Written that way first, and every field showed the frame's first byte.
|
|
breakByte:
|
|
CALL printString
|
|
LDA.1
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
RET
|
|
|
|
; The same for the two byte fields, most significant first the way the frame holds them.
|
|
breakWord:
|
|
CALL printString
|
|
LDA.1
|
|
CALL printByteHex
|
|
INCD.1
|
|
LDA.1
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
RET
|
|
|
|
; A and B together are a number. Prints it in decimal without leading zeroes, which covers
|
|
; a line number and a byte count both, so there is no need for one service each.
|
|
handlePrintNumber:
|
|
SETD.0 PrintNumber
|
|
STA.0
|
|
INCD.0
|
|
STB.0
|
|
SETD.0 PrintNumber
|
|
CALL printWordDecimal
|
|
RETI
|
|
|
|
; Giving the machine back. This is the one place MVDS earns its keep. The program's Stack,
|
|
; and the frame this very interrupt arrived on, are both abandoned where they lie, because
|
|
; nothing is going to return through either of them.
|
|
;
|
|
; Which is exactly why this cannot RETI. Its return address is on the Stack it just walked
|
|
; away from, so it branches to the prompt instead.
|
|
; What the last program made of what it was asked to do. Kept rather than shown: a program
|
|
; that failed has already said so in words, and a number beside that would be noise. It is
|
|
; here for the thing that cannot read words - whatever comes to run programs in sequence and
|
|
; has to decide whether to run the next one.
|
|
;
|
|
; IT IS NOT LineFailed, and the difference is worth keeping. This one is a PROGRAM'S answer
|
|
; and belongs to whoever asks for it, which the shell's own status command does and a test
|
|
; records. Zeroing it as each line began - which is what the script reader wanted - wiped
|
|
; the answer before the command that reports it could read its own line. Two questions, two
|
|
; bytes.
|
|
handleLastStatus:
|
|
SETD.2 LastStatus
|
|
LDA.2
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; ---- How the last start went, and settling it ----
|
|
;
|
|
; The answer goes in Q by writing into this handler's own frame, which is how every service
|
|
; here hands anything back: a handler arrives with the caller's registers pushed, not
|
|
; cleared, and RETI puts them back - so the way to return a value is to change the copy the
|
|
; return is going to restore.
|
|
handleBootState:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA bootStateNone
|
|
|
|
CALL sbfsBootState
|
|
BNQ bootStateNone
|
|
SETD.2 SbfsStateWas
|
|
LDA.2
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
bootStateNone:
|
|
; No disk, or one that would not answer. Nothing there to be unsettled about.
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
handleBootSettle:
|
|
SETD.2 DiskReady
|
|
LDA.2
|
|
BRA bootSettleNo
|
|
|
|
RSTA
|
|
CALL sbfsSetBootState
|
|
BNQ bootSettleNo
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
bootSettleNo:
|
|
INIA 0d1
|
|
RSTB
|
|
CCF
|
|
ADD ; A is the answer, so Q becomes it.
|
|
SRET
|
|
|
|
; The program is about to draw over everything. Q says whether what is there now will come
|
|
; back, and a program that is told no carries on anyway.
|
|
handleTakeScreen:
|
|
CALL screenTake
|
|
MVQA
|
|
MVSD.2
|
|
DPUP.2 0d02
|
|
STA.2
|
|
RETI
|
|
|
|
handleExit:
|
|
; ---- What the program made of it ----
|
|
;
|
|
; A, before anything below disturbs it. IN A RATHER THAN Q, and that is not a departure
|
|
; from the rule that a service answers in Q: this one takes an argument, the way
|
|
; osPrintNumber takes A and B, and it never returns to answer anything. The register a
|
|
; subroutine cannot hand anything back in is exactly the one that is free here - and Q is
|
|
; the ALU's output, so setting it to a small number costs four instructions where A costs
|
|
; one.
|
|
SETD.1 LastStatus
|
|
STA.1
|
|
|
|
; ---- The screen made usable, whether or not the picture can be given back ----
|
|
;
|
|
; A fraction of a cell is never what anybody wants left behind: the console draws in whole
|
|
; cells, so a view three pixels into one puts every character three pixels out for ever.
|
|
; That is true whether or not there was anywhere to save the picture, so it is not part of
|
|
; the saving - a program refused by osTakeScreen still must not leave the shell squinting.
|
|
;
|
|
; Before screenGive, so that a restored screen's own registers win.
|
|
CALL screenSane
|
|
|
|
; What was on the screen before this program had it, if it asked for that.
|
|
CALL screenGive
|
|
|
|
; The drive the person was on, whatever the program did with it.
|
|
PSHA
|
|
SETD.1 RunDrive
|
|
LDA.1
|
|
CALL sbfsUse
|
|
POPA
|
|
|
|
; And the line that started it failed, if the program says it did. The script reader asks
|
|
; one question - did this line work - and a program answering "no" is one of the ways it
|
|
; can be answered.
|
|
BRA exitWorked
|
|
INIA 0x01
|
|
SETD.1 LineFailed
|
|
STA.1
|
|
exitWorked:
|
|
|
|
SETD.1 SystemStack
|
|
LDD.0.1
|
|
MVDS.0
|
|
|
|
; Whatever the program put in the vector table comes out again. A vector points into the
|
|
; program that supplied it, and the program is gone, so anything left installed would aim
|
|
; an interrupt at whatever those addresses hold next.
|
|
CALL removeVectors
|
|
|
|
; And where the machine was before the program had it, for the same reason and by the
|
|
; same discipline as the Stack above and the vectors just now: a program is entitled to
|
|
; move about, and the shell is entitled to find itself where it left off. What a relative
|
|
; path means has changed back, so the remembered file goes with it.
|
|
SETD.0 SavedCwd
|
|
SETD.1 SbfsCwd
|
|
CALL sbfsCopyWord
|
|
CALL fileForget
|
|
|
|
; The console goes back to how the shell wants it, whatever the program left it in: line
|
|
; mode, and not interrupting. A program that wanted either is expected to put it back
|
|
; itself, but one that stopped early, or forgot, would otherwise hand back a shell with
|
|
; no echo and no backspace, or one being interrupted about keys it is reading anyway.
|
|
; This undoes everything the control port can be asked for and puts the cursor back, and
|
|
; asking for what is already the case costs a byte out of a port and does nothing. That
|
|
; is the right price for not having to know.
|
|
;
|
|
; THE CURSOR IS PUT BACK RATHER THAN LEFT, because a program that borrowed key mode and
|
|
; handed it back the way it was told to writes zero, which turns the cursor off. The shell
|
|
; owns the prompt, so the shell is what makes sure there is something blinking at it.
|
|
INIA 0x04
|
|
OUTA 0x02
|
|
|
|
; The ink too, and for the same reason as the cursor: a program that chose a colour is not
|
|
; around to put it back, and neither is one the fault screen printed for. The shell owns
|
|
; the prompt, so the shell is what makes sure it is readable.
|
|
RSTA
|
|
OUTA 0x06
|
|
|
|
; ---- Finished, or stopped ----
|
|
;
|
|
; A program that faulted did not finish, and saying so would be the shell's own word
|
|
; against what the fault screen just said in red immediately above it.
|
|
SETD.1 FaultStopped
|
|
LDA.1
|
|
BRA exitFinished
|
|
RSTA
|
|
STA.1 ; Cleared, so the next program is not blamed for this one.
|
|
SETD.0 Stopped
|
|
BRI exitSay
|
|
exitFinished:
|
|
SETD.0 Finished
|
|
exitSay:
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
; ---- dump ----
|
|
;
|
|
; dump Sixty four more bytes, carrying on from the last one.
|
|
; dump <where> From the start of that bank.
|
|
; dump <where> <addr> From there.
|
|
;
|
|
; <where> is program, data, or a bank number in hexadecimal. That the CPU cannot read
|
|
; Program Memory and this can is the whole point: the instruction set has no way to look
|
|
; at itself, and the controller does, so a monitor is possible at all only through it.
|
|
|
|
; b <program|data|number>
|
|
;
|
|
; The two banks that always exist have names, because "program" is what somebody means and
|
|
; 0 is only what the machine calls it. Anything else is a number, and has to be one that is
|
|
; really there.
|
|
doBank:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA bankWhat
|
|
|
|
SETD.1 ProgramWord
|
|
CALL textSame
|
|
BRQ bankProgram
|
|
SETD.1 DataWord
|
|
CALL textSame
|
|
BRQ bankData
|
|
|
|
CALL textHexWord
|
|
BNQ bankWhat
|
|
SETD.0 TextValue
|
|
INCD.0
|
|
LDA.0
|
|
BRI bankSet
|
|
bankProgram:
|
|
RSTA
|
|
BRI bankSet
|
|
bankData:
|
|
INIA 0d1
|
|
bankSet:
|
|
; The old one is kept, because asking about a bank means writing it down first - and if
|
|
; it turns out not to exist, being left pointed at it would fault on the very next look.
|
|
SETD.0 DumpBank
|
|
LDB.0
|
|
SETD.1 BankWas
|
|
STB.1
|
|
STA.0
|
|
|
|
; Naming a bank puts the cursor at the start of it, which is the only answer that does
|
|
; not depend on what was asked for last time.
|
|
CALL bankPresent
|
|
BRQ bankNotThere
|
|
|
|
RSTA
|
|
SETD.0 DumpAt
|
|
STA.0
|
|
INCD.0
|
|
STA.0
|
|
|
|
SETD.0 BankIs
|
|
CALL printString
|
|
SETD.0 DumpBank
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
bankNotThere:
|
|
SETD.0 BankWas
|
|
LDA.0
|
|
SETD.1 DumpBank
|
|
STA.1 ; Back where it was, which is somewhere that exists.
|
|
SETD.0 NoSuchBank
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
bankWhat:
|
|
SETD.0 BankUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; x [address] - sixty four bytes. d [address] - eight instructions. Without an address
|
|
; either carries on from where the last one stopped, so reading through memory is one
|
|
; letter at a time and the two share a place in it.
|
|
doExamine:
|
|
RSTA
|
|
SETD.0 ShowAsCode
|
|
STA.0
|
|
BRI showAt
|
|
|
|
doDisassemble:
|
|
INIA 0x01
|
|
SETD.0 ShowAsCode
|
|
STA.0
|
|
|
|
showAt:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
LDA.0
|
|
BRA dumpGo ; Nothing said, so carry on from where the last one stopped.
|
|
CALL textHexWord
|
|
BNQ dumpBadWhere
|
|
SETD.0 TextValue
|
|
LDA.0
|
|
SETD.1 DumpAt
|
|
STA.1
|
|
INCD.0
|
|
LDA.0
|
|
INCD.1
|
|
STA.1
|
|
|
|
dumpCheckBank:
|
|
CALL bankPresent
|
|
BRQ dumpNoBank
|
|
|
|
dumpGo:
|
|
SETD.0 ShowAsCode
|
|
LDA.0
|
|
BNA disassembleGo
|
|
|
|
INIA 0d4
|
|
SETD.0 DumpRows
|
|
STA.0
|
|
|
|
dumpRow:
|
|
SETD.0 DumpAt
|
|
CALL printWordHex
|
|
INIA 0d2
|
|
CALL printSpaces
|
|
|
|
; Point the controller at the row. Reading the Data port takes a byte and steps the
|
|
; source on, so the whole row is one instruction repeated.
|
|
SETD.0 DumpBank
|
|
LDA.0
|
|
OUTA 0xE0
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
OUTA 0xE1
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE2
|
|
|
|
; Sixteen bytes, kept as they go past so that they can be shown twice.
|
|
INIA 0d16
|
|
SETD.0 DumpCount
|
|
STA.0
|
|
SETD.1 DumpBytes
|
|
dumpByte:
|
|
INA 0xE9
|
|
STA.1
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
INCD.1
|
|
SETD.0 DumpCount
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA dumpByte
|
|
|
|
; The same sixteen again, as characters. Anything that is not printable shows as a dot,
|
|
; because a control character sent to the console would move the cursor and ruin the
|
|
; shape of the dump.
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
INIA 0d16
|
|
SETD.0 DumpCount
|
|
STA.0
|
|
SETD.1 DumpBytes
|
|
dumpChar:
|
|
LDA.1
|
|
INIB 0x20
|
|
CCF
|
|
SUB
|
|
BRC dumpDot ; Below a space.
|
|
INIB 0x7F
|
|
CCF
|
|
SUB
|
|
BNC dumpDot ; Delete, or above it.
|
|
OUTA 0x00
|
|
BRI dumpCharNext
|
|
dumpDot:
|
|
INIA 0x2E
|
|
OUTA 0x00
|
|
dumpCharNext:
|
|
INCD.1
|
|
SETD.0 DumpCount
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA dumpChar
|
|
CALL newLine
|
|
|
|
; Sixteen further along, carrying into the high byte if the low one wrapped.
|
|
SETD.0 DumpAt
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d16
|
|
CCF
|
|
ADD
|
|
STQ.0
|
|
BNC dumpRowNext
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
dumpRowNext:
|
|
SETD.0 DumpRows
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA dumpRow
|
|
BRI prompt
|
|
|
|
disassembleGo:
|
|
INIA 0d8
|
|
SETD.0 DumpRows
|
|
STA.0
|
|
disassembleOne:
|
|
CALL showInstruction
|
|
SETD.0 DumpRows
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA disassembleOne
|
|
BRI prompt
|
|
|
|
dumpBadWhere:
|
|
SETD.0 ExamineUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
dumpNoBank:
|
|
SETD.0 NoSuchBank
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; s <address> <byte> <byte> ...
|
|
;
|
|
; Writes into whichever bank is being looked at, THROUGH THE CONTROLLER, so Program Memory
|
|
; can be changed as easily as Data - which no instruction on this machine can do, and which
|
|
; is most of the reason for having a monitor at all.
|
|
;
|
|
; The cursor is left alone. Somebody poking a byte is usually looking at something else, and
|
|
; having the address they were reading move underneath them would be a poor reward.
|
|
doSet:
|
|
; A bank can be present and still refuse to be written: the controller's own table is
|
|
; published read only, and writing to it is refused. A refusal nobody catches stops the
|
|
; machine, which is a poor answer to somebody looking around with b and then typing s.
|
|
CALL bankPresent
|
|
SETD.0 BankFlags
|
|
LDA.0
|
|
INIB 0x02 ; The read only bit of that bank's record.
|
|
AND
|
|
BNQ setReadOnly
|
|
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL textHexWord
|
|
BNQ setWhat
|
|
|
|
SETD.1 DumpBank
|
|
LDA.1
|
|
OUTA 0xE3
|
|
SETD.1 TextValue
|
|
LDA.1
|
|
OUTA 0xE4
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE5
|
|
|
|
CALL stepPastNumber
|
|
PSHD.3
|
|
POPD.0
|
|
setByte:
|
|
CALL textHexWord
|
|
BNQ prompt ; Nothing more on the line, so that was all of them.
|
|
SETD.1 TextValue
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE9 ; The destination steps on by itself, so a run of bytes is a loop.
|
|
CALL stepPastNumber
|
|
PSHD.3
|
|
POPD.0
|
|
BRI setByte
|
|
|
|
setReadOnly:
|
|
SETD.0 ReadOnlyText
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
setWhat:
|
|
SETD.0 SetUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; g <address>
|
|
;
|
|
; Somewhere to go. It does not come back by itself - that would want a breakpoint, which is
|
|
; a byte written over an instruction and a handler waiting for it, and neither exists yet.
|
|
; But a program that gives the machine back the ordinary way lands at the prompt it was
|
|
; started from, which is this one, still in the monitor.
|
|
doGo:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL textHexWord
|
|
BNQ goWhat
|
|
|
|
; Where the Stack is now, written down before leaving, exactly as run does it. Whatever is
|
|
; jumped to may give the machine back through osExit, and osExit puts the Stack back to
|
|
; what is written here - so without this it would restore the one the LAST run left, or
|
|
; none at all, and the shell would come back with its Stack pointing at nothing.
|
|
MVSD.0
|
|
SETD.1 SystemStack
|
|
STD.0.1
|
|
|
|
SETD.1 TextValue
|
|
LDD.3.1
|
|
BRD.3
|
|
|
|
goWhat:
|
|
SETD.0 GoUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; DP0 names text that textHexWord has just read a number off the front of. LEAVES DP3 past
|
|
; the digits and any spaces after them, ready for the next one.
|
|
;
|
|
; DP3 rather than DP0, because a subroutine cannot hand a pointer back in DP0: CALL saves it
|
|
; and RET puts it back, so stepping it here would be undone on the way out.
|
|
stepPastNumber:
|
|
PSHD.0
|
|
POPD.3
|
|
SETD.1 TextDigits
|
|
LDB.1
|
|
stepPastDigits:
|
|
BRB stepPastSpaces
|
|
INCD.3
|
|
DECB
|
|
BRI stepPastDigits
|
|
stepPastSpaces:
|
|
LDA.3
|
|
BRA stepPastDone
|
|
INIB 0x20
|
|
XOR
|
|
BNQ stepPastDone
|
|
INCD.3
|
|
BRI stepPastSpaces
|
|
stepPastDone:
|
|
RET
|
|
|
|
|
|
; Points the controller's source at the cursor, so that reading port 0xE9 walks forwards.
|
|
aimAtDumpAt:
|
|
SETD.0 DumpBank
|
|
LDA.0
|
|
OUTA 0xE0
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
OUTA 0xE1
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE2
|
|
RET
|
|
|
|
; One byte from the cursor, and the cursor moves on.
|
|
;
|
|
; THE BYTE COMES BACK IN Q, not in A, because a subroutine cannot hand anything back in A:
|
|
; CALL saves it and RET puts it back, so an assignment here would be undone by the return.
|
|
takeByte:
|
|
INA 0xE9
|
|
PSHA
|
|
SETD.0 DumpAt
|
|
INCD.0
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
BNC takeByteDone
|
|
DECD.0
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
takeByteDone:
|
|
POPA
|
|
RSTB
|
|
OR
|
|
RET
|
|
|
|
; ---- Showing instructions ----
|
|
;
|
|
; The half of a monitor that a byte dump cannot do. What it needs and a dump does not
|
|
; is to know how LONG each instruction is, because getting that wrong does not print one
|
|
; line wrong - it loses the place and prints everything after it wrong.
|
|
|
|
; One instruction: where it is, the bytes it is made of, and what it says.
|
|
showInstruction:
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
CALL printByteHex
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
OUTA 0x00
|
|
|
|
CALL aimAtDumpAt
|
|
CALL takeByte
|
|
MVQA
|
|
SETD.0 Opcode
|
|
STA.0
|
|
|
|
CALL findInstruction
|
|
BNQ showUnknown
|
|
|
|
; What shape it is, and from that how many bytes it runs to.
|
|
PSHD.3
|
|
POPD.0
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 Shape
|
|
STA.1
|
|
|
|
SETD.0 ShapeLength
|
|
LDB.1
|
|
shapeStep:
|
|
BRB shapeGot
|
|
INCD.0
|
|
DECB
|
|
BRI shapeStep
|
|
shapeGot:
|
|
LDA.0
|
|
SETD.1 Length
|
|
STA.1
|
|
|
|
; The rest of its bytes. The first one is already read.
|
|
SETD.0 InstrBytes
|
|
SETD.1 Opcode
|
|
LDA.1
|
|
STA.0
|
|
INCD.0
|
|
SETD.1 Length
|
|
LDB.1
|
|
DECB
|
|
readRest:
|
|
BRB readRestDone
|
|
PSHB
|
|
CALL takeByte
|
|
POPB
|
|
MVQA
|
|
STA.0
|
|
INCD.0
|
|
DECB
|
|
BRI readRest
|
|
readRestDone:
|
|
|
|
; Show them, padded out so that what follows lines up however long the instruction was.
|
|
SETD.0 InstrBytes
|
|
SETD.1 Length
|
|
LDB.1
|
|
showBytes:
|
|
PSHB
|
|
LDA.0
|
|
CALL printByteHex
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
POPB
|
|
INCD.0
|
|
DECB
|
|
BNB showBytes
|
|
|
|
INIB 0d4
|
|
SETD.0 Length
|
|
LDA.0
|
|
padBytes:
|
|
CCF
|
|
SUB
|
|
BRQ padDone ; As many as there are, so nothing to pad.
|
|
PSHA
|
|
PSHB
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
OUTA 0x00
|
|
OUTA 0x00
|
|
POPB
|
|
POPA
|
|
DECB
|
|
BRI padBytes
|
|
padDone:
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
|
|
; Its name, without the spaces it is padded to four with.
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d02
|
|
INIB 0d4
|
|
showName:
|
|
LDA.0
|
|
BRA showNameDone
|
|
PSHB
|
|
INIB 0x20
|
|
XOR
|
|
POPB
|
|
BRQ showNameDone
|
|
OUTA 0x00
|
|
INCD.0
|
|
DECB
|
|
BNB showName
|
|
showNameDone:
|
|
|
|
; And whatever follows it, which depends only on the shape.
|
|
SETD.0 Shape
|
|
LDA.0
|
|
BRA showOperandNone ; 0, nothing at all
|
|
|
|
DECA
|
|
BRA showAddress ; 1, an address
|
|
DECA
|
|
BRA showByte ; 2, one byte
|
|
DECA
|
|
BRA showSelector ; 3, a Data Pointer
|
|
DECA
|
|
BRA showSelectorByte ; 4, a Data Pointer and a byte
|
|
DECA
|
|
BRA showSelectorAddress ; 5, a Data Pointer and an address
|
|
BRI showTwoSelectors ; 6
|
|
|
|
showOperandNone:
|
|
CALL newLine
|
|
RET
|
|
|
|
showAddress:
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
RET
|
|
|
|
showByte:
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
RET
|
|
|
|
showSelector:
|
|
CALL putSelectorOne
|
|
CALL newLine
|
|
RET
|
|
|
|
showSelectorByte:
|
|
CALL putSelectorOne
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
DPUP.0 0d02
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
RET
|
|
|
|
showSelectorAddress:
|
|
CALL putSelectorOne
|
|
INIA 0x20
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
DPUP.0 0d02
|
|
LDA.0
|
|
CALL printByteHex
|
|
INCD.0
|
|
LDA.0
|
|
CALL printByteHex
|
|
CALL newLine
|
|
RET
|
|
|
|
showTwoSelectors:
|
|
CALL putSelectorOne
|
|
INIA 0d46 ; .
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
DPUP.0 0d02
|
|
LDA.0
|
|
CALL printDecimalDigit
|
|
CALL newLine
|
|
RET
|
|
|
|
; ".n" for the selector that follows the opcode.
|
|
putSelectorOne:
|
|
INIA 0d46
|
|
OUTA 0x00
|
|
SETD.0 InstrBytes
|
|
INCD.0
|
|
LDA.0
|
|
CALL printDecimalDigit
|
|
RET
|
|
|
|
; A byte that decodes as nothing. Shown as it is, and the cursor moves on by one, because
|
|
; the only honest thing to do with a byte that is not an instruction is say so and carry on.
|
|
showUnknown:
|
|
SETD.0 Opcode
|
|
LDA.0
|
|
CALL printByteHex
|
|
SETD.0 UnknownText
|
|
SWI osPrintString
|
|
RET
|
|
|
|
; Looks the opcode up. DP3 lands on its entry and Q is zero, or Q is not zero and it is not
|
|
; an instruction at all.
|
|
findInstruction:
|
|
SETD.3 Instructions
|
|
SETD.0 InstructionCount
|
|
LDB.0
|
|
findStep:
|
|
LDA.3
|
|
SETD.0 Opcode
|
|
PSHB
|
|
LDB.0
|
|
XOR
|
|
POPB
|
|
BRQ findFound
|
|
DPUP.3 0d07
|
|
DECB
|
|
BNB findStep
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
findFound:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
|
|
; ---- Assembling a line at a time ----
|
|
;
|
|
; a <address>, then instructions until a line that is just a dot. The syntax is the
|
|
; assembler's - a selector rides on the mnemonic as LDA.0 or LDD.0.1, and leaving one off
|
|
; means Data Pointer 0, exactly as it does in a source file - so nothing learned here has to
|
|
; be unlearned when writing a real program.
|
|
;
|
|
; NUMBERS ARE HEXADECIMAL AND BARE. A source file writes 0x2000 or 0d16 because it has both
|
|
; and must say which; a monitor has only one and says so once, in the manual, rather than on
|
|
; every line. It is the same reason x and d take bare addresses.
|
|
;
|
|
; What cannot be written here is a label, and that is the whole difference between this and
|
|
; the assembler proper: a label is a promise to fill an address in later, and later is what
|
|
; a line at a time does not have.
|
|
|
|
doAssemble:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL textHexWord
|
|
BNQ assembleWhat
|
|
SETD.0 TextValue
|
|
SETD.1 DumpAt
|
|
CALL sbfsCopyWord ; Two bytes from DP0 to DP1, which sbfs already has.
|
|
|
|
assembleLine:
|
|
SETD.0 DumpAt
|
|
CALL printWordHex
|
|
SETD.0 AsmPrompt
|
|
CALL printString
|
|
|
|
SETD.0 AsmLine
|
|
INIB 0d40
|
|
CALL shellReadRaw
|
|
|
|
INA 0x01
|
|
INIB 0x02 ; ENDED, so there is nothing more to assemble.
|
|
AND
|
|
BNQ prompt
|
|
|
|
SETD.0 AsmLine
|
|
LDA.0
|
|
BRA assembleLine ; An empty line is somebody thinking.
|
|
|
|
SETD.0 AsmLine
|
|
SETD.1 DotText
|
|
CALL textSame
|
|
BRQ prompt
|
|
|
|
SETD.0 AsmLine
|
|
CALL textSplit ; The mnemonic, and whatever follows it.
|
|
CALL assembleOne
|
|
BRI assembleLine
|
|
|
|
assembleWhat:
|
|
SETD.0 AsmUsage
|
|
CALL printString
|
|
CALL newLine
|
|
BRI commandFailed
|
|
|
|
; The mnemonic is in CommandLine's place - AsmLine - and TextRest is what followed it.
|
|
; Puts the bytes down and steps the cursor past them.
|
|
assembleOne:
|
|
CALL takeMnemonic
|
|
CALL findByName
|
|
BNQ assembleUnknown
|
|
|
|
; WHATEVER IT NEEDS IS READ BEFORE ANYTHING IS WRITTEN. Emitting the opcode first and
|
|
; discovering the missing value afterwards leaves half an instruction in memory, which the
|
|
; next line usually covers up and the last line of a session does not.
|
|
SETD.0 AsmShape
|
|
LDA.0
|
|
BRA assemblePut ; 0, nothing to read.
|
|
DECA
|
|
BRA assembleWantAddress ; 1
|
|
DECA
|
|
BRA assembleWantByte ; 2
|
|
DECA
|
|
BRA assemblePut ; 3, a selector and nothing else.
|
|
DECA
|
|
BRA assembleWantByte ; 4
|
|
DECA
|
|
BRA assembleWantAddress ; 5
|
|
BRI assemblePut ; 6, two selectors.
|
|
|
|
assembleWantByte:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL textHexWord
|
|
BNQ assembleNeedsValue
|
|
BRI assemblePut
|
|
|
|
assembleWantAddress:
|
|
SETD.1 TextRest
|
|
LDD.0.1
|
|
CALL textHexWord
|
|
BNQ assembleNeedsValue
|
|
|
|
assemblePut:
|
|
; Point the controller at the cursor. Every byte written steps it on by itself.
|
|
SETD.0 DumpBank
|
|
LDA.0
|
|
OUTA 0xE3
|
|
SETD.0 DumpAt
|
|
LDA.0
|
|
OUTA 0xE4
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE5
|
|
|
|
SETD.0 AsmOpcode
|
|
LDA.0
|
|
OUTA 0xE9
|
|
|
|
; What follows depends only on the shape, exactly as it does when reading one back.
|
|
SETD.0 AsmShape
|
|
LDA.0
|
|
BRA assembleDone ; 0
|
|
DECA
|
|
BRA assembleAddress ; 1
|
|
DECA
|
|
BRA assembleByte ; 2
|
|
DECA
|
|
BRA assembleSelector ; 3
|
|
DECA
|
|
BRA assembleSelByte ; 4
|
|
DECA
|
|
BRA assembleSelAddress ; 5
|
|
|
|
SETD.0 AsmSelOne
|
|
LDA.0
|
|
OUTA 0xE9
|
|
SETD.0 AsmSelTwo
|
|
LDA.0
|
|
OUTA 0xE9
|
|
BRI assembleDone
|
|
|
|
assembleSelector:
|
|
SETD.0 AsmSelOne
|
|
LDA.0
|
|
OUTA 0xE9
|
|
BRI assembleDone
|
|
|
|
assembleSelByte:
|
|
SETD.0 AsmSelOne
|
|
LDA.0
|
|
OUTA 0xE9
|
|
BRI assembleByte
|
|
|
|
assembleSelAddress:
|
|
SETD.0 AsmSelOne
|
|
LDA.0
|
|
OUTA 0xE9
|
|
BRI assembleAddress
|
|
|
|
assembleByte:
|
|
SETD.0 TextValue
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE9
|
|
BRI assembleDone
|
|
|
|
assembleAddress:
|
|
SETD.0 TextValue
|
|
LDA.0
|
|
OUTA 0xE9
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE9
|
|
|
|
assembleDone:
|
|
; Past what was just written. The length is the shape's, out of the same table the
|
|
; disassembler reads, so the two can never disagree about how much room one takes.
|
|
SETD.0 ShapeLength
|
|
SETD.1 AsmShape
|
|
LDB.1
|
|
assembleStep:
|
|
BRB assembleStepped
|
|
INCD.0
|
|
DECB
|
|
BRI assembleStep
|
|
assembleStepped:
|
|
LDA.0
|
|
SETD.0 DumpAt
|
|
CALL stepCursor
|
|
RET
|
|
|
|
assembleUnknown:
|
|
SETD.0 NoSuchOp
|
|
CALL printString
|
|
CALL newLine
|
|
RET
|
|
|
|
assembleNeedsValue:
|
|
SETD.0 NeedsValue
|
|
CALL printString
|
|
CALL newLine
|
|
RET
|
|
|
|
; DP0 is a two byte address and A is how far to move it on.
|
|
stepCursor:
|
|
INCD.0
|
|
LDB.0
|
|
CCF
|
|
ADD
|
|
STQ.0
|
|
DECD.0
|
|
LDA.0
|
|
RSTB
|
|
ADD
|
|
STQ.0
|
|
RET
|
|
|
|
; The typed mnemonic into four padded characters and up to two selectors, which is the shape
|
|
; the table holds. Folded to upper case, because the assembler does not care about the case
|
|
; of a mnemonic and neither should this.
|
|
;
|
|
; A selector that was not typed is Data Pointer 0, exactly as an omitted one means in a
|
|
; source file. That is worth matching rather than demanding: half the instruction set names
|
|
; a pointer, and most code only ever uses the first.
|
|
takeMnemonic:
|
|
RSTA
|
|
SETD.1 AsmSelOne
|
|
STA.1
|
|
SETD.1 AsmSelTwo
|
|
STA.1
|
|
INIA 0d4
|
|
SETD.1 AsmLeft
|
|
STA.1
|
|
|
|
SETD.0 AsmLine
|
|
SETD.1 AsmName
|
|
|
|
takeMnemonicChar:
|
|
SETD.2 AsmLeft
|
|
LDA.2
|
|
BRA takeMnemonicPad ; Four is as long as a mnemonic gets.
|
|
LDA.0
|
|
BRA takeMnemonicPad ; The word ended.
|
|
INIB 0d46 ; A dot, so the selectors start here.
|
|
XOR
|
|
BRQ takeMnemonicPad
|
|
|
|
; Nothing below compares against A, so it survives all of this and is stored at the end.
|
|
INIB 0d97 ; a
|
|
CCF
|
|
SUB
|
|
BRC takeMnemonicPut ; It borrowed, so this is below 'a'.
|
|
INIB 0d123 ; One past z.
|
|
CCF
|
|
SUB
|
|
BNC takeMnemonicPut ; No borrow, so it is 'z' or later.
|
|
INIB 0x20
|
|
CCF
|
|
SUB
|
|
MVQA ; Upper case, which is how the table holds them.
|
|
|
|
takeMnemonicPut:
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
SETD.2 AsmLeft
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BRI takeMnemonicChar
|
|
|
|
takeMnemonicPad:
|
|
SETD.2 AsmLeft
|
|
LDA.2
|
|
BRA takeMnemonicSelectors
|
|
INIA 0x20
|
|
STA.1
|
|
INCD.1
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BRI takeMnemonicPad
|
|
|
|
takeMnemonicSelectors:
|
|
RSTA
|
|
STA.1 ; The four are a string now, like the ones in the table.
|
|
|
|
LDA.0
|
|
INIB 0d46
|
|
XOR
|
|
BNQ takeMnemonicEnd ; No dot, so both selectors stay at nought.
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d48
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
SETD.1 AsmSelOne
|
|
STA.1
|
|
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d46
|
|
XOR
|
|
BNQ takeMnemonicEnd
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d48
|
|
CCF
|
|
SUB
|
|
MVQA
|
|
SETD.1 AsmSelTwo
|
|
STA.1
|
|
|
|
takeMnemonicEnd:
|
|
RET
|
|
|
|
; Looks the four characters up. AsmOpcode and AsmShape are filled in and Q is zero, or Q is
|
|
; not zero and there is no such instruction.
|
|
;
|
|
; The same table the disassembler reads, searched the other way round. That is the point of
|
|
; it being a table rather than two lists: what can be written can be read back, and what can
|
|
; be read back can be written, and neither can drift from the other.
|
|
findByName:
|
|
SETD.3 Instructions
|
|
SETD.0 InstructionCount
|
|
LDA.0
|
|
SETD.1 AsmLeft
|
|
STA.1
|
|
|
|
findByNameStep:
|
|
PSHD.3
|
|
POPD.0
|
|
DPUP.0 0d02
|
|
SETD.2 AsmName
|
|
INIA 0d4
|
|
SETD.1 AsmCount
|
|
STA.1
|
|
|
|
findByNameChar:
|
|
LDA.0
|
|
LDB.2
|
|
XOR
|
|
BNQ findByNameNext
|
|
INCD.0
|
|
INCD.2
|
|
SETD.1 AsmCount
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA findByNameChar
|
|
|
|
PSHD.3
|
|
POPD.0
|
|
LDA.0
|
|
SETD.1 AsmOpcode
|
|
STA.1
|
|
PSHD.3
|
|
POPD.0
|
|
INCD.0
|
|
LDA.0
|
|
SETD.1 AsmShape
|
|
STA.1
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
findByNameNext:
|
|
DPUP.3 0d07
|
|
SETD.1 AsmLeft
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BNA findByNameStep
|
|
RSTA
|
|
INIB 0d1
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Is there such a bank? Q is zero if there is not.
|
|
;
|
|
; ASKING THE CONTROLLER FOR A BANK THAT IS NOT THERE IS REFUSED, and a refusal nobody
|
|
; catches stops the machine, which is a wretched answer to a mistyped number. The bank table
|
|
; says what exists, and it lives in bank 2.
|
|
;
|
|
; Bank n's record starts at n times eight. A and B are a shift register sixteen bits wide,
|
|
; so putting the number in the low half and rotating left three times multiplies it by
|
|
; eight without anything falling off the top: the most it can reach is 2040.
|
|
bankPresent:
|
|
RSTA
|
|
SETD.0 DumpBank
|
|
LDB.0
|
|
SHL SHL SHL
|
|
SETD.0 DumpRecord
|
|
STA.0
|
|
INCD.0
|
|
STB.0
|
|
|
|
INIA 0d2
|
|
OUTA 0xE0 ; SourceBank: the controller's own memory.
|
|
SETD.0 DumpRecord
|
|
LDA.0
|
|
OUTA 0xE1
|
|
INCD.0
|
|
LDA.0
|
|
OUTA 0xE2
|
|
INA 0xE9 ; The flags byte of that bank's record.
|
|
SETD.0 BankFlags
|
|
STA.0 ; Kept whole, since present is not the only thing it says.
|
|
INIB 0x01
|
|
AND
|
|
RET
|
|
|
|
; ---- help ----
|
|
|
|
doHelp:
|
|
SETD.0 HelpText
|
|
CALL printString
|
|
CALL newLine
|
|
SETD.0 HelpCdText
|
|
CALL printString
|
|
CALL newLine
|
|
SETD.0 HelpScriptText
|
|
CALL printString
|
|
CALL newLine
|
|
SETD.0 HelpMoreText
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
; And what the monitor adds, but only when it is on. Listing commands that would not
|
|
; answer is a way of teaching somebody something untrue.
|
|
SETD.0 Mode
|
|
LDA.0
|
|
BRA prompt
|
|
SETD.0 MonitorHelp
|
|
CALL printString
|
|
CALL newLine
|
|
BRI prompt
|
|
|
|
#Data
|
|
|
|
Banner:
|
|
"CosmOS"
|
|
PromptText:
|
|
"> "
|
|
; Nothing has run yet, so nothing has failed yet.
|
|
; Whether the line the shell is on failed. Read by the script reader and by nothing else,
|
|
; which is why it is a plain flag and not a number: a script wants to know whether to go on,
|
|
; not what went wrong, and what went wrong has already been said in words.
|
|
LineFailed:
|
|
0x00
|
|
DriveWanted:
|
|
0x00
|
|
|
|
|
|
LastStatus:
|
|
0x00
|
|
|
|
OnFallback:
|
|
"this is the fallback: what boot.cfg asks for did not start
|
|
"
|
|
NoDisk:
|
|
"no filesystem on the disk"
|
|
ScriptUsage:
|
|
"do: give me the name of a script"
|
|
ScriptNoFile:
|
|
"do: cannot find it"
|
|
ScriptNotOne:
|
|
"do: that is not a script - it wants #! on the first line"
|
|
ScriptTooDeep:
|
|
"do: scripts are only four deep"
|
|
DriveNoSuch:
|
|
"drive: this machine has no such drive"
|
|
DriveNotReadable:
|
|
"drive: nothing this can read is in that drive"
|
|
StartupName:
|
|
"/System/Boot/startup.sh"
|
|
StartupNotOne:
|
|
"startup.sh is there but does not begin with #!, so it was not run"
|
|
ScriptStopped:
|
|
"stopped: that line did not work"
|
|
Unknown:
|
|
"I do not know: "
|
|
Farewell:
|
|
"halted"
|
|
DirectoryText:
|
|
"<dir>"
|
|
UnfinishedText:
|
|
"<unfinished>"
|
|
BlocksText:
|
|
" blocks"
|
|
NotDirectory:
|
|
"that is not a directory"
|
|
MadeText:
|
|
"made"
|
|
RemovedText:
|
|
"removed"
|
|
MkdirWhat:
|
|
"mkdir what?"
|
|
RmdirWhat:
|
|
"rmdir what?"
|
|
MkdirNo:
|
|
"cannot make that: check the path, the name, and whether it is taken"
|
|
RmdirNo:
|
|
"cannot remove that: it must be a directory, and empty"
|
|
Separator:
|
|
"/"
|
|
|
|
; Where a program is looked for when it is not where you are. One fixed place rather than a
|
|
; list somebody sets, because a list would need somewhere to live between one boot and the
|
|
; next, and there is no such place yet.
|
|
ScreenFileName:
|
|
"sbfs.screen"
|
|
AppsPrefix:
|
|
"/Apps/"
|
|
SystemAppsPrefix:
|
|
"0:/Apps/"
|
|
IsDirectory:
|
|
"that is a directory"
|
|
AndText:
|
|
", "
|
|
FoldersText:
|
|
" directories"
|
|
FolderText:
|
|
" directory"
|
|
FilesText:
|
|
" files"
|
|
FileText:
|
|
" file"
|
|
|
|
; Two strings rather than one, because a string literal stops at 255 characters and each
|
|
; one carries its own zero byte, so they are printed in turn rather than joined.
|
|
HelpText:
|
|
"dir list what is on the disk
|
|
load <file> read a program off the disk
|
|
run [words] start what was loaded, and tell it those words
|
|
<name> [words] look where you are and then in /Apps, and start that"
|
|
HelpCdText:
|
|
"cd [path] go to a directory, or to the root with nothing after it
|
|
mkdir <path> make a directory
|
|
rmdir <path> remove an empty one"
|
|
HelpScriptText:
|
|
"do <file> run the lines in a file, which must start with #!
|
|
echo [words] say them
|
|
clear empty the screen"
|
|
HelpMoreText:
|
|
"delete <file> take it off the disk
|
|
rename <file> <to> call it something else
|
|
monitor look at memory, change it, and jump into it
|
|
help this
|
|
exit stop, or leave the monitor if you are in it"
|
|
|
|
ExamineUsage:
|
|
"x <address>, or x on its own to carry on"
|
|
NoSuchBank:
|
|
"there is no such bank"
|
|
ProgramWord:
|
|
"program"
|
|
DataWord:
|
|
"data"
|
|
|
|
ExecMagic:
|
|
"SBEX"
|
|
|
|
; What every program on the disk is called, and the four characters that make one
|
|
; reachable by typing its name.
|
|
SbxSuffix:
|
|
".sbx"
|
|
LoadWhat:
|
|
"load what?"
|
|
NoSuchFile:
|
|
"no such file"
|
|
Deleted:
|
|
"gone"
|
|
Renamed:
|
|
"renamed"
|
|
DeleteWhat:
|
|
"delete what?"
|
|
RenameWhat:
|
|
"rename what to what?"
|
|
RenameNo:
|
|
"there is no such file, or that name is taken"
|
|
TooManyVectors:
|
|
"that program wants more vectors than there is room for"
|
|
Unreadable:
|
|
"could not read it"
|
|
NotProgram:
|
|
"not a program"
|
|
WrongVersion:
|
|
"a version I do not know"
|
|
LoadedText:
|
|
"loaded, starting at "
|
|
NothingLoaded:
|
|
"nothing is loaded"
|
|
Stopped:
|
|
"the program was stopped"
|
|
FaultOpcode:
|
|
"that byte is not an instruction"
|
|
FaultGuard:
|
|
"a write into a fenced off part of a bank"
|
|
FaultBank:
|
|
"a bank that is not there, or an address past its end"
|
|
FaultNoHandler:
|
|
"nothing is installed at service "
|
|
FaultNoDevice:
|
|
"nothing is installed for the device on port "
|
|
FaultAt:
|
|
", at "
|
|
FaultRegisters:
|
|
" A "
|
|
FaultB:
|
|
" B "
|
|
FaultQ:
|
|
" Q "
|
|
FaultSystem:
|
|
"that was the system itself, so there is nowhere to carry on from. Start the machine again."
|
|
Finished:
|
|
"finished"
|
|
|
|
BreakText:
|
|
"break at "
|
|
ARegText:
|
|
"A "
|
|
BRegText:
|
|
"B "
|
|
QRegText:
|
|
"Q "
|
|
SRegText:
|
|
"status "
|
|
DP0Text:
|
|
"DP0 "
|
|
DP1Text:
|
|
"DP1 "
|
|
DP2Text:
|
|
"DP2 "
|
|
DP3Text:
|
|
"DP3 "
|
|
SPText:
|
|
"SP "
|
|
CarryText:
|
|
"carry "
|
|
FaultText:
|
|
"fault "
|
|
IntsText:
|
|
"interrupts "
|
|
ResumeText:
|
|
"press a key "
|
|
MonitorPrompt:
|
|
"* "
|
|
UnknownText:
|
|
" ?
|
|
"
|
|
MonitorHelp:
|
|
"x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves"
|
|
ExamineName:
|
|
"x"
|
|
DisName:
|
|
"d"
|
|
SetName:
|
|
"s"
|
|
BankName:
|
|
"b"
|
|
GoName:
|
|
"g"
|
|
BankUsage:
|
|
"b <program|data|number>"
|
|
BankIs:
|
|
"bank "
|
|
SetUsage:
|
|
"s <address> <byte> <byte> ..."
|
|
ReadOnlyText:
|
|
"that bank will not be written"
|
|
GoUsage:
|
|
"g <address>"
|
|
AsmName2:
|
|
"a"
|
|
AsmPrompt:
|
|
": "
|
|
DotText:
|
|
"."
|
|
AsmUsage:
|
|
"a <address>, then instructions, then a dot"
|
|
NoSuchOp:
|
|
"no such instruction"
|
|
NeedsValue:
|
|
"that one needs a value after it"
|
|
; ---- The shell's own words, packed ----
|
|
;
|
|
; Fifteen names with nothing between them, which is a TABLE and not an accident of layout.
|
|
; Each is a string ending in a zero, so the next one begins after that zero and walking them
|
|
; needs no pointers and no lengths - which is what makes completing a half typed command
|
|
; possible at all, since the dispatch below is a chain of comparisons and cannot be walked.
|
|
;
|
|
; NOTHING MAY BE PUT BETWEEN THEM. Tests/docs.sh checks that this run holds exactly the names
|
|
; the dispatch tests and that the count below agrees, so a command added without a name here
|
|
; is caught rather than silently left out of what Tab knows about.
|
|
;
|
|
; AND EACH LABEL ENDS IN "Name", which is how that check recognises one. A label called
|
|
; ForName2 was not counted, and the check said the run was one shorter than the count claimed
|
|
; rather than saying why - which is the right way round for it to fail, but the convention is
|
|
; worth writing down where the names are rather than only in the checker.
|
|
ShellNames:
|
|
DirName:
|
|
"dir"
|
|
DoName:
|
|
"do"
|
|
EchoName:
|
|
"echo"
|
|
ClearName:
|
|
"clear"
|
|
DriveName:
|
|
"drive"
|
|
LoadName:
|
|
"load"
|
|
RunName:
|
|
"run"
|
|
DeleteName:
|
|
"delete"
|
|
RenameName:
|
|
"rename"
|
|
CdName:
|
|
"cd"
|
|
MkdirName:
|
|
"mkdir"
|
|
RmdirName:
|
|
"rmdir"
|
|
HelpName:
|
|
"help"
|
|
ExitName:
|
|
"exit"
|
|
MonitorName:
|
|
"monitor"
|
|
SetVarName:
|
|
"set"
|
|
IfName:
|
|
"if"
|
|
ElseName:
|
|
"else"
|
|
EndName:
|
|
"end"
|
|
SameName:
|
|
"same"
|
|
WhileName:
|
|
"while"
|
|
ForName:
|
|
"for"
|
|
|
|
; How many of them, since a run of strings does not say where it stops.
|
|
ShellNameCount:
|
|
0d22
|
|
|
|
SbxText:
|
|
".sbx"
|
|
AppsPath:
|
|
"/Apps"
|
|
VarNoSuch:
|
|
"nothing is set called "
|
|
VarTooLong:
|
|
"that line is too long once its names are filled in"
|
|
VarNameLong:
|
|
"that name is longer than a name can be"
|
|
SetVarNoRoom:
|
|
"there is no room for another name"
|
|
SetVarIs:
|
|
" is "
|
|
IfUsage:
|
|
"if wants a command to decide by"
|
|
IfTooDeep:
|
|
"that is more blocks than will fit inside one another"
|
|
ElseLonely:
|
|
"else with no if above it"
|
|
EndLonely:
|
|
"end with no if above it"
|
|
LoopTyped:
|
|
"a loop wants a script to go back through"
|
|
ForUsage:
|
|
"for wants a name, then in, then the words"
|
|
InName:
|
|
"in"
|
|
|
|
; Where the machine is, written out for the prompt, and where in the buffer it begins.
|
|
; Built from the end backwards, so it starts somewhere in the middle.
|
|
CwdText:
|
|
#Reserve 0d127
|
|
CwdAt:
|
|
0x00 0x00
|
|
CwdWalk:
|
|
0x00 0x00
|
|
PathLength:
|
|
0x00
|
|
|
|
; What is left of CwdText to write into, and whether the path ran out of it. Counted down
|
|
; rather than compared against an address, because the check is on every byte written and
|
|
; a subtraction of two pointers is a great deal more than a byte that is already going to
|
|
; be tested for zero.
|
|
CwdRoom:
|
|
0x00
|
|
CwdCut:
|
|
0x00
|
|
|
|
; What the working directory was when a program was started, so that it can be put back
|
|
; when the program stops.
|
|
SavedCwd:
|
|
0x00 0x00
|
|
|
|
DirFolders:
|
|
0x00
|
|
DirTaken:
|
|
0x00
|
|
DiskReady:
|
|
0x00
|
|
LoadedOk:
|
|
0x00
|
|
|
|
; What loadProgram found, and the words for it. See loadProgram for what the numbers mean.
|
|
LoadStatus:
|
|
0x00
|
|
LoadMessage:
|
|
0x00 0x00
|
|
|
|
; THE PATH of the file to load, which load copies out of the line as typed and a typed
|
|
; program name is built into.
|
|
;
|
|
; Sixty four rather than the twenty three a NAME needs. It held a name when a disk was
|
|
; flat and there was nothing else to hold, and leaving it that size once paths existed cut
|
|
; every path longer than twenty two characters down to twenty two - which is not a failure
|
|
; that looks like one. "/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/",
|
|
; resolved perfectly well, and reported that the program was a directory.
|
|
ProgramName:
|
|
#Reserve 0d64
|
|
NameOk:
|
|
0x00
|
|
NamePrefix:
|
|
0x00
|
|
NameLeft:
|
|
0x00
|
|
LoadedEntry:
|
|
0x00 0x00
|
|
LoadCount:
|
|
0x00
|
|
LoadVersion:
|
|
0x00
|
|
|
|
; Where the first of rename's two names is, kept while the second is picked out of the
|
|
; line, since finding that needs the pointers for itself.
|
|
RenameFrom:
|
|
0x00 0x00
|
|
|
|
; What followed "run", kept for the program to ask for.
|
|
RunArgument:
|
|
#Reserve 0d64
|
|
|
|
; A number on its way to being printed, since the routine that prints one wants it in
|
|
; memory and a service is handed it in registers.
|
|
PrintNumber:
|
|
0x00 0x00
|
|
|
|
; ---- Where the last file anybody asked about lives ----
|
|
;
|
|
; osFileBlock is handed a name every time it is called, because a stateless service has no
|
|
; handle to leak and nothing left open by a program that stops in the middle. Taken at its
|
|
; word that means searching the directory once per block, so reading a four hundred block
|
|
; file walks the directory four hundred times over to be told the same thing.
|
|
;
|
|
; So the last answer is kept. A call naming the same file as the one before it skips the
|
|
; search and puts these back where sbfs keeps them. MEASURED, on the 329 block file the
|
|
; streaming test reads: 7% of the whole run saved when the file is the first entry in the
|
|
; directory, 11% when it is the sixteenth. Modest, and worth having for the shape rather
|
|
; than the size - what it really removes is a cost that grows with how full the disk is,
|
|
; on the one operation that is repeated once per block. NOTHING IS EVER TRUSTED THAT WAS NOT
|
|
; PUT HERE BY A SEARCH: this is a copy of an answer, not a second place where the truth
|
|
; about a file is written, and every path that could make it wrong calls fileForget. That
|
|
; is what makes it a speed rather than a promise - a cache that has been thrown away is
|
|
; indistinguishable from one that was never filled.
|
|
;
|
|
; The name is twenty three bytes for a name of twenty two, so that a name filling the
|
|
; field still has a zero after it and can be compared as a string.
|
|
FileCacheDrive:
|
|
0x00
|
|
SearchDrive:
|
|
0x00
|
|
RunDrive:
|
|
0x00
|
|
ScreenSaved:
|
|
0x00
|
|
ScreenWasDrive:
|
|
0x00
|
|
ScreenAt:
|
|
0x00
|
|
ScreenBlockAt:
|
|
0x00 0x00
|
|
ScreenBlock:
|
|
#Reserve 0d256
|
|
FileCacheValid:
|
|
0x00
|
|
FileCacheName:
|
|
#Reserve 0d64
|
|
FileCacheStart:
|
|
0x00 0x00
|
|
FileCacheBlocks:
|
|
0x00 0x00
|
|
FileCacheTail:
|
|
0x00
|
|
|
|
; ---- The vectors a loaded program brought with it ----
|
|
;
|
|
; Six bytes each: where it goes, what goes there, and what was there before. The last two
|
|
; are filled in when the program runs and read back when it exits, so what a program covers
|
|
; up comes back rather than becoming a hole.
|
|
;
|
|
; Sixteen is a limit rather than a considered number. It is far more than anything written
|
|
; so far wants, and a program asking for more is refused at load rather than having some of
|
|
; its handlers installed and the rest dropped.
|
|
VectorSource:
|
|
0x00 0x00
|
|
VectorsLeft:
|
|
0x00
|
|
LoadedVectorCount:
|
|
0x00
|
|
LoadedVectors:
|
|
#Reserve 0d96
|
|
|
|
; Where the monitor is looking, so that a bare 'dump' can carry on from it.
|
|
DumpBank:
|
|
0x00
|
|
DumpAt:
|
|
0x00 0x00
|
|
DumpRows:
|
|
0x00
|
|
Mode:
|
|
0x00
|
|
DumpCount:
|
|
0x00
|
|
BankWas:
|
|
0x00
|
|
BankFlags:
|
|
0x00
|
|
AsmSelOne:
|
|
0x00
|
|
AsmSelTwo:
|
|
0x00
|
|
AsmLeft:
|
|
0x00
|
|
AsmCount:
|
|
0x00
|
|
AsmOpcode:
|
|
0x00
|
|
AsmShape:
|
|
0x00
|
|
AsmName:
|
|
#Reserve 0d5
|
|
AsmLine:
|
|
#Reserve 0d41
|
|
ShowAsCode:
|
|
0x00
|
|
DumpRecord:
|
|
0x00 0x00
|
|
Opcode:
|
|
0x00
|
|
Shape:
|
|
0x00
|
|
Length:
|
|
0x00
|
|
InstrBytes:
|
|
#Reserve 0d4
|
|
|
|
; How many bytes an instruction of each shape runs to, the opcode included.
|
|
ShapeLength:
|
|
0d1 0d3 0d2 0d2 0d3 0d4 0d3
|
|
|
|
InstructionCount:
|
|
0d72
|
|
|
|
; ---- The instruction table ----
|
|
;
|
|
; Generated by Tests/instructiontable.py from the assembler's own list, and checked against
|
|
; it by Tests/docs.sh. Seven bytes each: the opcode, the shape, and four characters of name
|
|
; with the zero the assembler puts after a string.
|
|
Instructions:
|
|
0x10 0d0 "ADD "
|
|
0x11 0d0 "SUB "
|
|
0x12 0d0 "AND "
|
|
0x13 0d0 "OR "
|
|
0x14 0d0 "XOR "
|
|
0x15 0d0 "NOTA"
|
|
0x16 0d0 "NOTB"
|
|
0x17 0d0 "SHL "
|
|
0x18 0d0 "SHR "
|
|
0x60 0d1 "BRI "
|
|
0x61 0d1 "BRQ "
|
|
0x62 0d1 "BRA "
|
|
0x63 0d1 "BRB "
|
|
0x64 0d1 "BRC "
|
|
0x65 0d3 "BRD "
|
|
0x66 0d1 "BNQ "
|
|
0x67 0d1 "BNA "
|
|
0x68 0d1 "BNB "
|
|
0x69 0d1 "BNC "
|
|
0x70 0d1 "RCAL"
|
|
0x71 0d1 "CALL"
|
|
0x72 0d2 "SWI "
|
|
0x73 0d0 "RETI"
|
|
0x74 0d0 "RRET"
|
|
0x75 0d0 "RET "
|
|
0x76 0d0 "SRET"
|
|
0x20 0d0 "RSTA"
|
|
0x21 0d0 "RSTB"
|
|
0x22 0d0 "INCA"
|
|
0x23 0d0 "INCB"
|
|
0x24 0d0 "DECA"
|
|
0x25 0d0 "DECB"
|
|
0x26 0d2 "INIA"
|
|
0x27 0d2 "INIB"
|
|
0x28 0d0 "CCF "
|
|
0x29 0d0 "MVQA"
|
|
0x2A 0d0 "MVQB"
|
|
0x2B 0d0 "SIF "
|
|
0x2C 0d0 "CIF "
|
|
0x30 0d0 "PSHQ"
|
|
0x31 0d0 "PSHA"
|
|
0x32 0d0 "PSHB"
|
|
0x33 0d3 "PSHD"
|
|
0x34 0d0 "POPA"
|
|
0x35 0d0 "POPB"
|
|
0x36 0d3 "POPD"
|
|
0x40 0d3 "INCD"
|
|
0x41 0d3 "DECD"
|
|
0x42 0d3 "LDA "
|
|
0x43 0d3 "LDB "
|
|
0x44 0d3 "STQ "
|
|
0x45 0d3 "STA "
|
|
0x46 0d3 "STB "
|
|
0x47 0d5 "SETD"
|
|
0x48 0d4 "DPUP"
|
|
0x49 0d4 "DPDN"
|
|
0x4A 0d6 "LDD "
|
|
0x4B 0d6 "STD "
|
|
0x4C 0d3 "MVSD"
|
|
0x4D 0d3 "MVDS"
|
|
0x4E 0d3 "DPUA"
|
|
0x4F 0d3 "DPDA"
|
|
0x50 0d3 "DPUW"
|
|
0x51 0d3 "DPDW"
|
|
0xD0 0d2 "OUTQ"
|
|
0xD1 0d2 "OUTA"
|
|
0xD2 0d2 "OUTB"
|
|
0xE0 0d2 "INA "
|
|
0xE1 0d2 "INB "
|
|
0xF0 0d0 "NOP "
|
|
0xFE 0d0 "WAIT"
|
|
0xFF 0d0 "HALT"
|
|
DumpBytes:
|
|
#Reserve 0d16
|
|
|
|
; Where the system's Stack was when it handed the machine to a program. Kept below the
|
|
; region a program owns, so that a program has to go looking to break it.
|
|
SystemStack:
|
|
0x00 0x00
|
|
; And where it is when the shell is between lines, which is not the same thing. See prompt.
|
|
ShellStack:
|
|
0x00 0x00
|
|
|
|
; ---- Which blocks are open, and whether their lines are being run ----
|
|
BlockStack:
|
|
#Reserve 0d128
|
|
BlockDepth:
|
|
0x00
|
|
BlockHold:
|
|
0x00
|
|
; What kind of block is about to be pushed, since blockPush takes the state in A and there is
|
|
; nowhere else for a second answer to go.
|
|
LoopKind:
|
|
0x00
|
|
; Set by an end that is sending a for round again, and read by the for line it lands on.
|
|
LoopResume:
|
|
0x00
|
|
ForUsed:
|
|
0x00
|
|
ForVarAt:
|
|
0x00 0x00
|
|
ForWordAt:
|
|
0x00 0x00
|
|
; Set while the line being run is an "if" condition rather than somebody's own command.
|
|
IfPending:
|
|
0x00
|
|
SameFirst:
|
|
0x00 0x00
|
|
DirSeen:
|
|
0x00
|
|
DirSize:
|
|
0x00 0x00
|
|
WidthLeft:
|
|
0x00
|
|
|
|
; ---- What a line being typed is made of ----
|
|
;
|
|
; All of it lives here rather than being passed about, because the routine that reads a line
|
|
; is a loop over keystrokes and there are four Data Pointers, two of which it needs for the
|
|
; buffer and the walking.
|
|
EditBase:
|
|
0x00 0x00
|
|
EditRoom:
|
|
0x00
|
|
EditLength:
|
|
0x00
|
|
EditAt:
|
|
0x00
|
|
EditRow:
|
|
0x00
|
|
EditColumn:
|
|
0x00
|
|
EditWidth:
|
|
0x00
|
|
EditChar:
|
|
0x00
|
|
|
|
; Where the counting up and down the rows has got to. Separate from EditRow and EditColumn,
|
|
; which are where the LINE starts: one of the two things that reads these is working out
|
|
; that very answer, and a walk that wrote its intermediate steps into its own starting point
|
|
; would be walking away from a moving mark.
|
|
EditWalkRow:
|
|
0x00
|
|
EditWalkColumn:
|
|
0x00
|
|
EditWalkBack:
|
|
0x00
|
|
|
|
; What the control port has to be written to put the console back how this routine found it.
|
|
EditWasControl:
|
|
0x00
|
|
|
|
; ---- What Tab is working on ----
|
|
TabWordAt:
|
|
0x00
|
|
TabWordLen:
|
|
0x00
|
|
TabCount:
|
|
0x00
|
|
TabLeft:
|
|
0x00
|
|
TabSeen:
|
|
0x00
|
|
TabAt:
|
|
0x00
|
|
TabHold:
|
|
0x00
|
|
TabPut:
|
|
0x00
|
|
TabNarrowAt:
|
|
0x00
|
|
TabBestLen:
|
|
0x00
|
|
; Where a candidate is, put down so that a routine which needs to walk it can pick it up
|
|
; without moving the pointer its caller is still using.
|
|
TabPointer:
|
|
0x00 0x00
|
|
; And where the candidate being looked at begins, since the comparison walks past it.
|
|
TabStart:
|
|
0x00 0x00
|
|
; The answer so far: the first match, cut back by every one after it to where they agree.
|
|
TabBest:
|
|
#Reserve 0d64
|
|
; The word being matched, as somebody would type it: a name with its extension taken off, or
|
|
; a directory with a separator put on.
|
|
TabCandidate:
|
|
#Reserve 0d48
|
|
; The part of the word up to and including its last separator, which is the directory to look
|
|
; in. Empty means where the machine already is.
|
|
TabDir:
|
|
#Reserve 0d64
|
|
TabDirLen:
|
|
0x00
|
|
TabCopyAt:
|
|
0x00
|
|
TabFirst:
|
|
0x00
|
|
TabPrograms:
|
|
0x00
|
|
; Whether the walk is looking for the answer or showing what the answers were.
|
|
TabShowing:
|
|
0x00
|
|
; Where the machine was standing before a directory was walked on its behalf.
|
|
TabWasCwd:
|
|
0x00 0x00
|
|
TabWasDrive:
|
|
0x00
|
|
|
|
; ---- What a name stands for ----
|
|
;
|
|
; Eight records of sixty four bytes: sixteen of name, then forty eight of value.
|
|
VarSlots:
|
|
#Reserve 0d512
|
|
VarSlot:
|
|
0x00
|
|
VarWanted:
|
|
0x00 0x00
|
|
VarKeepValue:
|
|
0x00 0x00
|
|
VarKeepLine:
|
|
0x00 0x00
|
|
VarRoom:
|
|
0x00
|
|
VarHold:
|
|
0x00
|
|
; The line with its names filled in, built here and copied back when it is whole.
|
|
VarLine:
|
|
#Reserve 0d128
|
|
VarPut:
|
|
0x00 0x00
|
|
VarUsed:
|
|
0x00
|
|
VarName:
|
|
#Reserve 0d16
|
|
VarNameLen:
|
|
0x00
|
|
SetVarWhich:
|
|
0x00 0x00
|
|
|
|
; ---- What the fault screen is saying ----
|
|
;
|
|
; Whether this cause names an entry as well as itself, and which one. Only the two faults
|
|
; about a missing handler do, and for those the machine puts the number in Q.
|
|
FaultNumbered:
|
|
0x00
|
|
FaultNumber:
|
|
0x00
|
|
; Set when a program is being ended by a fault rather than by asking. handleExit reads it to
|
|
; choose its last word, and clears it.
|
|
FaultStopped:
|
|
0x00
|
|
|
|
; Whether the line being read is one to remember. The shell's are; a program's are not.
|
|
EditKeepHistory:
|
|
0x00
|
|
|
|
; How many characters of the line are on the screen, which is not always how many are in it:
|
|
; between a line getting shorter and the screen being put right, the screen still has the
|
|
; old one on it. That gap is one character wide for a Delete and a whole line wide for a
|
|
; recalled one.
|
|
EditDrawn:
|
|
0x00
|
|
|
|
; How much a redraw sent to the screen, line and rubbing out together. The start of the line
|
|
; is that far back from where the cursor ended up.
|
|
EditPrinted:
|
|
0x00
|
|
|
|
; Eight lines of a hundred and twenty eight bytes, and one more holding whatever was being
|
|
; typed when somebody pressed Up.
|
|
HistoryLines:
|
|
#Reserve 0d1024
|
|
HistoryTyped:
|
|
#Reserve 0d128
|
|
|
|
; Which slot holds the oldest, which is what moves when a ninth line pushes one out.
|
|
HistoryStart:
|
|
0x00
|
|
; How many of the eight hold anything.
|
|
HistoryCount:
|
|
0x00
|
|
; Which one is on the screen, counting the oldest as nought. HistoryCount means none of them
|
|
; is: what is on the screen is the line being typed.
|
|
HistoryPick:
|
|
0x00
|
|
|
|
; A hundred and twenty seven characters and the zero byte that ends them. It was sixty three
|
|
; until the shell learned to edit a line, which is when the limit started to be felt: a copy
|
|
; between two disks with a directory on each is most of the way there before anything has
|
|
; been said, and the line can now be moved about in, so a long one is worth having.
|
|
CommandLine:
|
|
#Reserve 0d128
|
|
|
|
#Vectors
|
|
|
|
Boot boot
|
|
BadOpcode faultBadOpcode
|
|
GuardViolation faultGuard
|
|
BankFault faultBank
|
|
NoHandler faultNoHandler
|
|
NoDevice faultNoDevice
|
|
osPrintString handlePrintString
|
|
osReadLine handleReadLine
|
|
osExit handleExit
|
|
osArgument handleArgument
|
|
osFileRead handleFileRead
|
|
osFileSave handleFileSave
|
|
osFileDelete handleFileDelete
|
|
osFileRename handleFileRename
|
|
osFileInfo handleFileInfo
|
|
osFileBlock handleFileBlock
|
|
osChangeDir handleChangeDir
|
|
osFileStart handleFileStart
|
|
osFileWrite handleFileWrite
|
|
osFileDone handleFileDone
|
|
osFileFetch handleFileFetch
|
|
osPrintNumber handlePrintNumber
|
|
osBreak handleBreak
|
|
osLastStatus handleLastStatus
|
|
osTakeScreen handleTakeScreen
|
|
osBootState handleBootState
|
|
osBootSettle handleBootSettle
|
|
Device 0x20 diskDone
|