; 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 SETD.0 ScriptName SWI osFileInfo 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: 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 SETD.0 ScriptName SETD.1 ScriptBlock SETD.2 ScriptIndex LDA.2 INCD.2 LDB.2 SWI osFileBlock 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 ; 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 ; ---- 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 ; 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