A script is fetched a block at a time while its lines run, and its name is resolved afresh for every block. A name with a drive in front of it moves the machine to that drive on the way past - sbfsWalk calls sbfsUse - so a script found in the system's place on drive 0, started by somebody standing on a disk of their own, ran its lines on the system disk. Always possible with "do 0:/Apps/setup.sh", and reachable by typing a name now that the search finds scripts the same three places it finds programs. The drive is kept across each fetch and put back after it, at both places a script's name is resolved. The test has to work for it. A script that fits in one block is read entirely while it is being opened, and the opening was never the hard part; and the keep in scriptFill cannot be broken on its own, because scriptOpen has already written the variable down. So the script on the disk crosses two block boundaries and moves itself between them: what it says about where it is standing is 1 before the move and 0 after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
634 lines
16 KiB
NASM
634 lines
16 KiB
NASM
; Reading shell lines out of a file.
|
|
;
|
|
; DP0 = the file's name
|
|
; CALL scriptOpen Q = 0 and a script is running, or Q says what was wrong:
|
|
; 1 there is no such file
|
|
; 2 it is not a script - no #! on the front
|
|
; 3 too many scripts inside each other
|
|
; DP0 = a buffer, B = how much room
|
|
; CALL scriptLine Q = 0 and there is a line in the buffer, or nonzero at the end
|
|
;
|
|
; Written by Anachronaut
|
|
;
|
|
; ---- Why the shell reads files and not the other way round ----
|
|
;
|
|
; fileStream.asm does almost this and is deliberately not used. It is application machinery:
|
|
; More, Type and Wander each include it and each get their own copy in their own Data
|
|
; Segment. The shell reading a script through it would be a fourth copy, and the shell is the
|
|
; one place where that copy has to survive running a program - which is exactly the thing
|
|
; those programs are. Here the state belongs to the system and nothing a program does can
|
|
; reach it.
|
|
;
|
|
; ---- A block, and a nought on the end of it ----
|
|
;
|
|
; The reader walks a Data Pointer along the block and stops at a nought. A block is 256 bytes
|
|
; and a count of them does not fit in a byte, so every other way of knowing where the block
|
|
; ends costs sixteen bit arithmetic on every character. Writing a nought after the last byte
|
|
; costs one store per block and turns the whole question into "is this byte zero".
|
|
;
|
|
; The buffer is 257 bytes for that reason: a full block leaves the nought at the end of it.
|
|
; A script cannot contain a nought, which is not a restriction anybody will notice - it is
|
|
; text, and the #! on the front is what stops a program being read as one.
|
|
|
|
#Program
|
|
|
|
; ---- One script inside another ----
|
|
;
|
|
; A build script calling a setup script is the first thing anybody tries, so what is saved
|
|
; when one script starts another is a POSITION AND NOT A BUFFER. The whole state of a
|
|
; running script is its name, which block comes next, how many are left, and where in the
|
|
; block it is - seventy bytes, laid out next to each other below so that saving it is one
|
|
; copy. The block itself is read again on the way back, which costs one disk read per return
|
|
; and saves 257 bytes a level.
|
|
;
|
|
; Four levels. Deep enough for a script calling a script that calls a helper, and shallow
|
|
; enough that a script which runs itself says so instead of filling memory.
|
|
scriptPush:
|
|
CALL scriptSlotAt
|
|
SETD.0 ScriptName
|
|
PSHD.3
|
|
POPD.1
|
|
CALL scriptCopyState
|
|
RET
|
|
|
|
scriptPop:
|
|
CALL scriptSlotAt
|
|
PSHD.3
|
|
POPD.0
|
|
SETD.1 ScriptName
|
|
CALL scriptCopyState
|
|
; ScriptAt points into the block buffer, which now holds somebody else's block. Reading
|
|
; it back is what makes the saved pointer mean what it meant.
|
|
CALL scriptReread
|
|
RET
|
|
|
|
; DP3 = where the script one level up is remembered. Reached by stepping rather than by
|
|
; multiplying, because this machine cannot multiply and the depth is never more than three
|
|
; steps. DP3 because RET puts the others back.
|
|
scriptSlotAt:
|
|
SETD.3 ScriptSaved
|
|
SETD.2 ScriptDepth
|
|
LDA.2
|
|
DECA
|
|
BRA scriptSlotDone
|
|
scriptSlotStep:
|
|
DPUP.3 0d71
|
|
DECA
|
|
BNA scriptSlotStep
|
|
scriptSlotDone:
|
|
RET
|
|
|
|
; Seventy bytes, DP0 to DP1.
|
|
scriptCopyState:
|
|
INIB 0d71
|
|
scriptCopyByte:
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
DECB
|
|
BNB scriptCopyByte
|
|
RET
|
|
|
|
; The block that is meant to be in the buffer, back in the buffer. ScriptIndex is the NEXT
|
|
; one, so the one being read from is the one before it.
|
|
scriptReread:
|
|
SETD.1 ScriptIndex
|
|
LDA.1
|
|
INCD.1
|
|
LDB.1
|
|
DECB
|
|
BNC scriptRereadGo
|
|
DECA
|
|
scriptRereadGo:
|
|
SETD.0 ScriptName
|
|
SETD.1 ScriptBlock
|
|
SWI osFileBlock
|
|
SETD.1 ScriptBlock
|
|
PSHD.3
|
|
POPB
|
|
POPA
|
|
DPUW.1
|
|
RSTA
|
|
STA.1
|
|
RET
|
|
|
|
; ---- Opening ----
|
|
;
|
|
; The name is COPIED rather than remembered by address. osFileBlock is given the name again
|
|
; for every block, and the caller's copy is CommandLine, which the next line typed will
|
|
; overwrite. fileStream remembers an address and says in its own comment that the address
|
|
; must stay valid; here it cannot, because the thing that reads the next line is the reason
|
|
; the name is needed.
|
|
scriptOpen:
|
|
; ---- Four deep and no further ----
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
INIB 0d4
|
|
CCF
|
|
SUB
|
|
BRQ scriptOpenTooDeep
|
|
|
|
; ---- The one already running is put somewhere safe FIRST ----
|
|
;
|
|
; Before anything below overwrites it, and put back again on every way out of here that is
|
|
; not success. Opening writes the name into the live state to ask the disk about it, so by
|
|
; the time the answer is known the caller's place is already gone.
|
|
;
|
|
; A still holds the depth from the check above: SUB writes Q and leaves it alone.
|
|
BRA scriptOpenOutermost
|
|
CALL scriptPush
|
|
BRI scriptOpenFirst
|
|
scriptOpenOutermost:
|
|
; ---- The one place a stale quiet would matter ----
|
|
;
|
|
; A script started from the prompt begins loud whatever the last one left behind, which is
|
|
; what makes this the only place the flag has to be put back. Nested scripts INHERIT
|
|
; instead: a build that asked for quiet meant its helpers too.
|
|
RSTA
|
|
SETD.1 ScriptQuiet
|
|
STA.1
|
|
scriptOpenFirst:
|
|
|
|
SETD.1 ScriptName
|
|
INIB 0d63
|
|
CALL copyText
|
|
|
|
CALL scriptKeepDrive
|
|
SETD.0 ScriptName
|
|
SWI osFileInfo
|
|
CALL scriptGiveDriveBack
|
|
BRQ scriptOpenThere
|
|
INIA 0x01
|
|
BRI scriptOpenFailed
|
|
scriptOpenThere:
|
|
; DP3 is how many blocks. A file of none is not a script.
|
|
PSHD.3
|
|
POPB
|
|
POPA
|
|
SETD.1 ScriptBlocks
|
|
STA.1
|
|
INCD.1
|
|
STB.1
|
|
OR
|
|
BRQ scriptOpenNotOne
|
|
|
|
SETD.1 ScriptIndex
|
|
RSTA
|
|
STA.1
|
|
INCD.1
|
|
STA.1
|
|
|
|
; The first block, so that the #! can be looked at before anything is promised.
|
|
CALL scriptFill
|
|
BNQ scriptOpenNotOne
|
|
|
|
; ---- What makes a file a script ----
|
|
;
|
|
; Two bytes, and the rest of the line ignored. A directive rather than a comment, the way
|
|
; #Program is in assembly: the shell reads it and refuses the file without it, so calling
|
|
; it a comment would be a lie about what it does. What follows the #! is where the name of
|
|
; an interpreter goes when there is a second one; today there is one and it is this shell.
|
|
SETD.1 ScriptBlock
|
|
LDA.1
|
|
INIB 0d35 ; '#'
|
|
CCF
|
|
SUB
|
|
BNQ scriptOpenNotOne
|
|
INCD.1
|
|
LDA.1
|
|
INIB 0d33 ; '!'
|
|
CCF
|
|
SUB
|
|
BNQ scriptOpenNotOne
|
|
|
|
; Past the shebang line, wherever it ends.
|
|
CALL scriptSkipLine
|
|
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
RSTA
|
|
BRI scriptOpenAnswer ; A is nought, which is the answer for "it opened".
|
|
|
|
scriptOpenTooDeep:
|
|
INIA 0x03
|
|
BRI scriptOpenAnswer ; Nothing was pushed, so there is nothing to put back.
|
|
|
|
scriptOpenNotOne:
|
|
INIA 0x02
|
|
scriptOpenFailed:
|
|
; Whatever was running is still running, and its place is in the slot rather than in the
|
|
; live state. A is the answer and must survive being put back.
|
|
SETD.1 ScriptDepth
|
|
LDB.1
|
|
BRB scriptOpenAnswer
|
|
PSHA
|
|
CALL scriptPop
|
|
POPA
|
|
scriptOpenAnswer:
|
|
; Q is the answer, and A holds it. Adding nought is how a register becomes Q.
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- One line ----
|
|
;
|
|
; Comments and blank lines never reach the shell. The reader drops them, so the echo does not
|
|
; print them and the dispatch never sees a line it would have to know to ignore.
|
|
scriptLine:
|
|
SETD.1 ScriptRoom
|
|
STB.1
|
|
SETD.1 ScriptInto
|
|
STD.0.1
|
|
|
|
scriptLineAgain:
|
|
; ---- Where this line begins ----
|
|
;
|
|
; Kept before it is read, because a loop has to be able to go back to the line that opened
|
|
; it and by the time that line has been read the reader is past it. Three words, and the
|
|
; block itself is read again on the way back, which is what scriptReread is for.
|
|
SETD.0 ScriptIndex
|
|
SETD.1 ScriptLineIndex
|
|
CALL sbfsCopyWord
|
|
SETD.0 ScriptAt
|
|
SETD.1 ScriptLineAt
|
|
CALL sbfsCopyWord
|
|
SETD.0 ScriptBlocks
|
|
SETD.1 ScriptLineBlocks
|
|
CALL sbfsCopyWord
|
|
|
|
SETD.1 ScriptLength
|
|
RSTA
|
|
STA.1
|
|
SETD.0 ScriptInto
|
|
LDD.0.0
|
|
|
|
scriptLineNext:
|
|
CALL scriptByte
|
|
BNQ scriptLineEnded
|
|
|
|
SETD.1 ScriptChar
|
|
LDA.1
|
|
INIB 0d10
|
|
CCF
|
|
SUB
|
|
BRQ scriptLineDone
|
|
; A still holds the character: SUB writes Q and leaves it alone.
|
|
INIB 0d13 ; Carriage return, from a file written on a host that uses them.
|
|
CCF
|
|
SUB
|
|
BRQ scriptLineNext
|
|
|
|
; Room?
|
|
SETD.1 ScriptLength
|
|
LDA.1
|
|
SETD.1 ScriptRoom
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRQ scriptLineNext ; Full. Read on and drop what comes, the way readLine does.
|
|
|
|
SETD.1 ScriptChar
|
|
LDA.1
|
|
STA.0
|
|
INCD.0
|
|
SETD.1 ScriptLength
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI scriptLineNext
|
|
|
|
scriptLineDone:
|
|
; Terminate it, then decide whether the shell wants to see it.
|
|
RSTA
|
|
STA.0
|
|
|
|
SETD.0 ScriptInto
|
|
LDD.0.0
|
|
LDA.0
|
|
BRA scriptLineAgain ; Empty.
|
|
INIB 0d59 ; ';' - a comment, the same as everywhere else on this machine.
|
|
CCF
|
|
SUB
|
|
BRQ scriptLineAgain
|
|
|
|
; ---- A directive, which is about the file rather than for the shell ----
|
|
;
|
|
; '#' the way the assembler means it. #quiet stops each line being echoed as it runs, for a
|
|
; script whose own output is the point and which the prompts get in the way of; #loud puts
|
|
; it back.
|
|
;
|
|
; ANYTHING ELSE BEGINNING WITH # IS HANDED TO THE SHELL, which does not know it and says
|
|
; so and stops the script. That is deliberate and it is free: a script that asked for
|
|
; something this shell cannot do should not carry on as though it had been given it, and
|
|
; the machinery for saying so already exists.
|
|
INIB 0d35 ; '#'
|
|
CCF
|
|
SUB
|
|
BNQ scriptLineGive
|
|
|
|
SETD.1 QuietWord
|
|
CALL textSame
|
|
BRQ scriptLineQuiet
|
|
SETD.0 ScriptInto
|
|
LDD.0.0
|
|
SETD.1 LoudWord
|
|
CALL textSame
|
|
BRQ scriptLineLoud
|
|
SETD.0 ScriptInto
|
|
LDD.0.0
|
|
BRI scriptLineGive
|
|
|
|
scriptLineQuiet:
|
|
INIA 0x01
|
|
SETD.1 ScriptQuiet
|
|
STA.1
|
|
BRI scriptLineAgain
|
|
scriptLineLoud:
|
|
RSTA
|
|
SETD.1 ScriptQuiet
|
|
STA.1
|
|
BRI scriptLineAgain
|
|
|
|
scriptLineGive:
|
|
RSTA ; Q = 0: there is a line.
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
scriptLineEnded:
|
|
; ---- A last line with no newline on it is still a line ----
|
|
;
|
|
; Text files do not reliably end with one, and a script whose final command silently did
|
|
; not run because somebody's editor left the newline off is a bad way to find that out.
|
|
; If anything has been gathered, finish it the ordinary way; the next call comes back here
|
|
; with nothing gathered and ends for real.
|
|
SETD.1 ScriptLength
|
|
LDA.1
|
|
BRA scriptLineNoMore
|
|
BRI scriptLineDone
|
|
|
|
scriptLineNoMore:
|
|
CALL scriptClose
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- One character, or the end ----
|
|
;
|
|
; Q = 0 and the character is in ScriptChar, or Q is one and there are no more.
|
|
;
|
|
; IN MEMORY RATHER THAN IN A REGISTER, because RET puts A and B back the way the caller had
|
|
; them - only Q and Data Pointer 3 survive a CALL. Handing the character back in A looked
|
|
; right, assembled, and returned the caller's own A every time.
|
|
scriptByte:
|
|
SETD.1 ScriptAt
|
|
LDD.1.1
|
|
LDA.1
|
|
BRA scriptByteRefill ; The nought at the end of the block.
|
|
|
|
; Step the saved pointer past it.
|
|
SETD.1 ScriptChar
|
|
STA.1
|
|
SETD.1 ScriptAt
|
|
LDD.0.1
|
|
INCD.0
|
|
STD.0.1
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD ; Q = 0.
|
|
RET
|
|
|
|
scriptByteRefill:
|
|
CALL scriptFill
|
|
BNQ scriptByteNoMore
|
|
BRI scriptByte
|
|
scriptByteNoMore:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- The next block, with a nought written after it ----
|
|
;
|
|
; Q = 0 if there is one.
|
|
scriptFill:
|
|
SETD.1 ScriptBlocks
|
|
LDA.1
|
|
INCD.1
|
|
LDB.1
|
|
OR
|
|
BRQ scriptFillNoMore
|
|
|
|
CALL scriptKeepDrive
|
|
SETD.0 ScriptName
|
|
SETD.1 ScriptBlock
|
|
SETD.2 ScriptIndex
|
|
LDA.2
|
|
INCD.2
|
|
LDB.2
|
|
SWI osFileBlock
|
|
CALL scriptGiveDriveBack
|
|
BNQ scriptFillNoMore
|
|
|
|
; DP3 is how many bytes came back. The nought goes after them.
|
|
SETD.1 ScriptBlock
|
|
PSHD.3
|
|
POPB
|
|
POPA
|
|
DPUW.1
|
|
RSTA
|
|
STA.1
|
|
|
|
SETD.1 ScriptAt
|
|
SETD.0 ScriptBlock
|
|
STD.0.1
|
|
|
|
; Index++, blocks--.
|
|
SETD.1 ScriptIndex
|
|
INCD.1
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BNC scriptFillCount
|
|
DECD.1
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
scriptFillCount:
|
|
SETD.1 ScriptBlocks
|
|
INCD.1
|
|
LDA.1
|
|
BRA scriptFillBorrow
|
|
DECA
|
|
STA.1
|
|
BRI scriptFillGot
|
|
scriptFillBorrow:
|
|
INIA 0xFF
|
|
STA.1
|
|
DECD.1
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
scriptFillGot:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
scriptFillNoMore:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Reading a script must not move the person ----
|
|
;
|
|
; The name is resolved afresh for every block, and a name with a drive in front of it moves
|
|
; the machine to that drive as a side effect of being resolved - sbfsWalk calls sbfsUse on
|
|
; the way past. So a script found in the system's place on drive 0, started by somebody
|
|
; standing on a disk of their own, would run its lines on the system disk instead of theirs.
|
|
;
|
|
; It matters more now than it used to. `do 0:/Apps/setup.sh` was always able to do this;
|
|
; typing a name now finds scripts the same three places a program is looked for, and the
|
|
; third of them is drive-qualified.
|
|
;
|
|
; The answer that survives the CALL: Q into A and the byte count onto the stack, because
|
|
; sbfsUse writes Q and RET puts DP3 back the way it found it.
|
|
scriptKeepDrive:
|
|
INA 0x24
|
|
SETD.1 ScriptDrive
|
|
STA.1
|
|
RET
|
|
|
|
scriptGiveDriveBack:
|
|
PSHD.3
|
|
MVQA
|
|
PSHA
|
|
SETD.1 ScriptDrive
|
|
LDA.1
|
|
CALL sbfsUse
|
|
POPA
|
|
POPD.3
|
|
; A holds what Q held. Adding nought to it is how it becomes Q again.
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Everything up to and including the next line feed, thrown away. Used for the shebang.
|
|
scriptSkipLine:
|
|
CALL scriptByte
|
|
BNQ scriptSkipDone
|
|
SETD.1 ScriptChar
|
|
LDA.1
|
|
INIB 0d10
|
|
CCF
|
|
SUB
|
|
BNQ scriptSkipLine
|
|
scriptSkipDone:
|
|
RET
|
|
|
|
; One script ending. Whatever asked for it carries on, if anything did.
|
|
scriptClose:
|
|
SETD.1 ScriptDepth
|
|
LDA.1
|
|
BRA scriptCloseNone
|
|
DECA
|
|
STA.1
|
|
BRA scriptCloseNone
|
|
CALL scriptPop
|
|
RET
|
|
|
|
scriptCloseNone:
|
|
; ---- Nothing to put back here ----
|
|
;
|
|
; The flag is only ever READ while a script is running: the console's own path says its
|
|
; prompt whatever this holds, and the first script started from the prompt sets it to loud
|
|
; on the way in. So a stale quiet cannot be observed, and clearing it here would be a line
|
|
; no test could tell the difference about - which is how it was written the first time, with
|
|
; a comment claiming it fixed something. It fixed something that a later change had already
|
|
; made impossible.
|
|
RET
|
|
|
|
; Every script ending at once, which is what a line that did not work means. A build whose
|
|
; helper failed should not carry on in the script that called the helper either.
|
|
scriptAbandon:
|
|
RSTA
|
|
SETD.1 ScriptDepth
|
|
STA.1
|
|
RET
|
|
|
|
#Data
|
|
|
|
QuietWord:
|
|
"#quiet"
|
|
LoudWord:
|
|
"#loud"
|
|
|
|
ScriptDepth:
|
|
0x00
|
|
|
|
; The drive the person is standing on, kept across a block being fetched. Not part of the
|
|
; saved state below: it is only ever live for the length of one read.
|
|
ScriptDrive:
|
|
0x00
|
|
|
|
; ---- Seventy bytes, and they are next to each other on purpose ----
|
|
;
|
|
; Name, blocks left, next block, where in the block, and whether it is echoing: the whole of
|
|
; where a script has got to. Saving it is one copy because of this order, and nothing else
|
|
; may be put between them.
|
|
ScriptName:
|
|
#Reserve 0d64
|
|
ScriptBlocks:
|
|
0x00 0x00
|
|
ScriptIndex:
|
|
0x00 0x00
|
|
ScriptAt:
|
|
0x00 0x00
|
|
|
|
; ---- And where the line being read began ----
|
|
;
|
|
; A loop goes back to the line that opened it, and by the time that line has been read the
|
|
; reader is past it. So the position is kept before every line rather than worked out
|
|
; afterwards, which cannot be done: a line is not a fixed size and there is nothing to
|
|
; subtract.
|
|
ScriptLineIndex:
|
|
0x00 0x00
|
|
ScriptLineAt:
|
|
0x00 0x00
|
|
ScriptLineBlocks:
|
|
0x00 0x00
|
|
; Saved with the rest, so that a quiet script calling a loud one gets its quiet back when
|
|
; the loud one finishes. A new script INHERITS it rather than resetting, because a build
|
|
; that asked for quiet meant its helpers too; only the first script started from the prompt
|
|
; begins loud.
|
|
ScriptQuiet:
|
|
0x00
|
|
|
|
; Three would do - a save happens on the second script and not the first - but four costs
|
|
; seventy bytes and removes an off-by-one from the only place it could hide.
|
|
ScriptSaved:
|
|
#Reserve 0d284
|
|
ScriptInto:
|
|
0x00 0x00
|
|
ScriptRoom:
|
|
0x00
|
|
ScriptLength:
|
|
0x00
|
|
ScriptChar:
|
|
0x00
|
|
ScriptBlock:
|
|
#Reserve 0d257
|