diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 9e2d346..08e3d1f 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -170,8 +170,15 @@ it, so the next line comes from the console again. The interactive assembler reads its lines the same way, so a script can contain a block of assembly and end it with a `.` just as you would by hand. -**A script cannot yet run another script.** That is the next thing, and it wants a stack of -positions rather than the single one the reader keeps today. +**A script can run another script, four deep.** What is remembered when one script starts +another is a position and not a buffer - the name, which block comes next, how many are left, +and where in the block it had got to. The block itself is read again on the way back, which +costs one disk read per return and saves a 257-byte buffer per level. Four is deep enough for +a script calling a script that calls a helper, and shallow enough that a script which runs +itself says `do: scripts are only four deep` rather than filling memory. + +**A line that fails stops every level**, not just the innermost. A build whose helper script +failed should not carry on in the script that called the helper. ## Shell Commands: diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 682856a..07b2484 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -140,7 +140,7 @@ prompt: ; 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 ScriptRunning + SETD.1 ScriptDepth LDA.1 BRA promptWhere SETD.1 LineFailed @@ -149,7 +149,9 @@ prompt: SETD.0 ScriptStopped CALL printString CALL newLine - CALL scriptClose + ; 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: ; Where you are, but only when that is not obvious. At the root the prompt is the one it @@ -320,11 +322,12 @@ promptSay: ; 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 ScriptRunning + SETD.1 ScriptDepth LDA.1 BRA shellReadTyped CALL scriptLine - BNQ shellReadTyped + BNQ shellReadLine ; That one ended. Ask again: something under it may still be + ; running, and only depth reaching nought means the console. ; Echoed, so that a script working can be watched and a script failing says where. It is ; printed after the prompt, so it reads exactly like somebody typing it. @@ -1624,6 +1627,13 @@ doScript: CCF SUB BRQ scriptNoFile + INIB 0x02 + CCF + SUB + BRQ scriptNotOne + SETD.0 ScriptTooDeep + BRI fileComplain +scriptNotOne: SETD.0 ScriptNotOne BRI fileComplain scriptNoFile: @@ -3628,6 +3638,8 @@ 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" ScriptStopped: "stopped: that line did not work" Unknown: diff --git a/Programs/CosmOS/Source/script.asm b/Programs/CosmOS/Source/script.asm index 1d0b1e1..804445b 100644 --- a/Programs/CosmOS/Source/script.asm +++ b/Programs/CosmOS/Source/script.asm @@ -4,6 +4,7 @@ ; 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 ; @@ -31,6 +32,87 @@ #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 0d70 + DECA + BNA scriptSlotStep +scriptSlotDone: + RET + +; Seventy bytes, DP0 to DP1. +scriptCopyState: + INIB 0d70 +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 @@ -39,6 +121,25 @@ ; 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 scriptOpenFirst + CALL scriptPush +scriptOpenFirst: + SETD.1 ScriptName INIB 0d63 CALL copyText @@ -92,15 +193,29 @@ scriptOpenThere: ; Past the shebang line, wherever it ends. CALL scriptSkipLine - INIA 0x01 - SETD.1 ScriptRunning + SETD.1 ScriptDepth + LDA.1 + INCA STA.1 RSTA - BRI scriptOpenFailed ; A is nought, which is the answer for "it opened". + 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 @@ -322,16 +437,35 @@ 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 +scriptCloseNone: + 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 ScriptRunning + SETD.1 ScriptDepth STA.1 RET #Data -ScriptRunning: +ScriptDepth: 0x00 + +; ---- Seventy bytes, and they are next to each other on purpose ---- +; +; Name, blocks left, next block, where in the block: 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: @@ -340,6 +474,11 @@ ScriptIndex: 0x00 0x00 ScriptAt: 0x00 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 0d280 ScriptInto: 0x00 0x00 ScriptRoom: diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index 49db860..8d49138 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -79,7 +79,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 174 tests, of which 112 run, 35 +everything it printed against a file in `Tests/expected`. 175 tests, of which 113 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 946e9b0..4eaf744 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -13,7 +13,10 @@ bad.script 45 plain.script 24 cross.script 280 nonl.script 38 -13 files +outer.script 376 +inner.script 44 +loop.script 35 +16 files > load what? > no such file > not a program diff --git a/Tests/expected/cosmosScriptNest.out b/Tests/expected/cosmosScriptNest.out new file mode 100644 index 0000000..f4f8c80 --- /dev/null +++ b/Tests/expected/cosmosScriptNest.out @@ -0,0 +1,27 @@ +CosmOS +> > echo outer in block one +outer in block one +> do inner.script +> echo inner one +inner one +> echo inner two +inner two +> echo outer resumed in block one +outer resumed in block one +> > echo self +self +> do loop.script +> echo self +self +> do loop.script +> echo self +self +> do loop.script +> echo self +self +> do loop.script +do: scripts are only four deep +stopped: that line did not work +> halted +Execution halted. +[exit 0] diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index c0b126c..cf76281 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -12,7 +12,10 @@ bad.script 45 plain.script 24 cross.script 280 nonl.script 38 -13 files +outer.script 376 +inner.script 44 +loop.script 35 +16 files > loaded, starting at 4000 > it says: the disk took its time finished diff --git a/Tests/input/cosmosScriptNest.in b/Tests/input/cosmosScriptNest.in new file mode 100644 index 0000000..9d3a726 --- /dev/null +++ b/Tests/input/cosmosScriptNest.in @@ -0,0 +1,3 @@ +do outer.script +do loop.script +exit diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 89c03e1..0297685 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -135,6 +135,26 @@ open('cross.script','w').write(head + pad + 'Say across the block boundary\n') # The last line has no newline after it and still has to run. printf '#! script\nSay with no newline after me' > nonl.script "$TOOL" put "$DISKS/cosmos.img" nonl.script >/dev/null +# ---- One script inside another ---- +# +# outer.script resumes in its SECOND block, which is the case the whole design turns on: the +# inner script reads its own block into the one buffer there is, so coming back means reading +# the outer one's block again and landing on the byte it left off at. Generated, because +# where the do line falls is the entire point and no editor should be able to move it. +python3 -c " +head = '#! script\n' +pad = '' +while len(head) + len(pad) < 300: + pad += '; pad\n' +open('outer.script','w').write(head + pad + + 'echo outer in block one\ndo inner.script\necho outer resumed in block one\n') +" +"$TOOL" put "$DISKS/cosmos.img" outer.script >/dev/null +printf '#! script\necho inner one\necho inner two\n' > inner.script +"$TOOL" put "$DISKS/cosmos.img" inner.script >/dev/null +# A script that runs itself, which is what the depth limit is for. +printf '#! script\necho self\ndo loop.script\n' > loop.script +"$TOOL" put "$DISKS/cosmos.img" loop.script >/dev/null # A disk of its own for the writing test, with one file already on it so that what it # writes has to be placed somewhere that does not tread on what is there. diff --git a/Tests/manifest b/Tests/manifest index d2c4044..2e34fdf 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -775,6 +775,12 @@ cosmosScriptBad | CosmOS/Source/cosmos.asm | run | cosmosScr # the boundary between two blocks, and a last line with no newline after it. Both of these # were faults before they were checks. cosmosScriptEdges | CosmOS/Source/cosmos.asm | run | cosmosScriptEdges.in | 200000000 | disks/cosmos.img +# One script inside another, and the limit on how far that goes. The outer script resumes in +# its second block, which is the case the design turns on - the inner one reads its own block +# into the single buffer, so coming back means reading the outer one's again and landing on +# the byte it left. Then a script that runs itself, which stops at four deep and takes every +# level with it, because a build whose helper failed should not carry on in its caller. +cosmosScriptNest | CosmOS/Source/cosmos.asm | run | cosmosScriptNest.in | 200000000 | disks/cosmos.img printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | - printDigitTest | testPrograms/printDigitTest.asm | xfail | - | - printHexTest | testPrograms/printHexTest.asm | xfail | - | -