diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 27c15e0..1dcd781 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -116,6 +116,35 @@ an installed library and a copy of the source. The disk is rebuilt from scratch when its applications change, so its contents describe the current source tree rather than accumulating files left by older builds. +## Lines Run Sometimes: + +```text +#! script +set colour red +if same $colour red + echo it is red +else + echo it is not +end +``` + +**`if` takes a command**, and what follows runs only if that command worked. That is one rule +rather than two, and it is why comparing values needs no syntax of its own: `same` is an +ordinary command that fails when its two words differ, so `if same $a $b` falls out of the +rule instead of being an exception to it. Anything else that can fail can be asked about the +same way - `if load Snake.sbx` is a perfectly good question. + +The shell already had the other half. `LineFailed` exists because a script stops at the first +line that did not work, so every command in the system was already saying whether it had +worked, for a different reason. + +Blocks nest eight deep. A branch that is not being taken is **not even looked at**: the +skipping happens before names are filled in, so `$whatever` inside a branch nobody is running +is not a mistake, and a line nobody is running cannot fail. + +**Lines may be indented**, which they could not be before there was anything to indent +inside. Leading spaces are taken off before anything looks at the line. + ## Names For Things: `set name value` writes one down, and `$name` anywhere on a later line stands for it. @@ -273,6 +302,10 @@ CosmOS currently provides these built-in commands: | `delete ` | Remove a file from the filesystem and release its blocks. | | `rename ` | Give a file a different name without moving its contents. | | `set [name [value]]` | Give a name a value, or say what the names are. See Names For Things. | +| `if ` | Run the lines after it only if that command worked. See Lines Run Sometimes. | +| `else` | Run them only if it did not. | +| `end` | Close the block. | +| `same ` | Fails when the two are different, which is how `if` asks about a value. | | `monitor` | Enter monitor mode, in which the prompt becomes `*` and the commands below are also available. | | `help` | Show the built-in command summary. | | `exit` | Leave monitor mode if in it, and otherwise halt the machine. | diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 64020c8..9a479df 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -187,6 +187,32 @@ prompt: 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 + 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 @@ -251,8 +277,24 @@ promptWhere: ; 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 @@ -326,6 +368,26 @@ promptWhere: 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 HelpName CALL textSame @@ -2704,13 +2766,13 @@ varExpandNameLong: SETD.0 VarNameLong CALL printString CALL newLine - CALL commandFailed ; Which does not come back. + BRI commandFailed varExpandLong: SETD.0 VarTooLong CALL printString CALL newLine - CALL commandFailed ; Which does not come back. + BRI commandFailed varExpandNoSuch: SETD.0 VarNoSuch @@ -2718,7 +2780,296 @@ varExpandNoSuch: SETD.0 VarName CALL printString CALL newLine - CALL commandFailed ; Which does not come back. + 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 one byte on a stack eight deep: +; +; 0 this branch is running +; 1 this branch is not, but an else would turn it on +; 2 this branch is not, and an else would not either +; +; 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. +blockSkipping: + SETD.1 BlockDepth + LDA.1 + BRA blockRunning ; Nothing open, so nothing is being skipped. + DECA + SETD.3 BlockStack + DPUA.3 + 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 what to push. 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 + SETD.3 BlockStack + DPUA.3 + SETD.1 BlockHold + LDA.1 + STA.3 + SETD.1 BlockDepth + LDA.1 + INCA + STA.1 + RSTA + RSTB + CCF + ADD + RET +blockFull: + INIA 0x01 + RSTB + CCF + ADD + RET + +; DP3 on the block on top. Q is one if there is not one. +blockTop: + SETD.1 BlockDepth + LDA.1 + BRA blockNone + DECA + SETD.3 BlockStack + DPUA.3 + RSTA + RSTB + CCF + ADD + RET +blockNone: + INIA 0x01 + RSTB + CCF + ADD + 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: + CALL blockSkipping + BNQ ifSkipped + + SETD.1 TextRest + LDD.0.1 + LDA.0 + BRA ifWhat ; "if" with 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 + CALL printString + CALL newLine + BRI commandFailed + +ifDeep: + SETD.0 IfTooDeep + CALL printString + CALL newLine + BRI commandFailed + +; ---- 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 + CALL printString + CALL newLine + BRI commandFailed + +; ---- end ---- +doEnd: + SETD.1 BlockDepth + LDA.1 + BRA endLonely + DECA + STA.1 + BRI prompt +endLonely: + SETD.0 EndLonely + CALL printString + CALL newLine + BRI commandFailed + +; ---- 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 + +; 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 ElseName + CALL lineFirstIs + BRQ doElse + + SETD.0 CommandLine + SETD.1 EndName + CALL lineFirstIs + BRQ doEnd + + BRI prompt ; ---- set ---- ; @@ -2749,8 +3100,7 @@ setVarFull: SETD.0 SetVarNoRoom CALL printString CALL newLine - CALL commandFailed - BRI prompt + BRI commandFailed setVarList: RSTA @@ -6957,10 +7307,18 @@ MonitorName: "monitor" SetVarName: "set" +IfName: +"if" +ElseName: +"else" +EndName: +"end" +SameName: +"same" ; How many of them, since a run of strings does not say where it stops. ShellNameCount: - 0d16 + 0d20 SbxText: ".sbx" @@ -6976,6 +7334,14 @@ 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" ; 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. @@ -7258,6 +7624,19 @@ SystemStack: ; 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 0d8 +BlockDepth: + 0x00 +BlockHold: + 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: diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index 8724b17..897e06e 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`. 201 tests, of which 139 run, 35 +everything it printed against a file in `Tests/expected`. 202 tests, of which 140 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/cosmosBlocks.out b/Tests/expected/cosmosBlocks.out new file mode 100644 index 0000000..c5aa3ec --- /dev/null +++ b/Tests/expected/cosmosBlocks.out @@ -0,0 +1,54 @@ +CosmOS +> set a hello +> if same $a hello +> echo taken +taken +> else +> echo not taken +> end +> if load nosuch.sbx +no such file +> echo loaded +> else +> echo did not +did not +> end +> else +else with no if above it +> end +end with no if above it +> if +if wants a command to decide by +> do blocks.script +> set colour red +> if same $colour red +> echo it is red +it is red +> if same $colour blue +> echo and blue +> else +> echo but not blue +but not blue +> end +> else +> echo $neverSetAnywhere +> end +> if same $colour blue +> echo wrong +> else +> echo right +right +> end +> if same $colour blue +> if same $colour red +> echo deep wrong +> else +> echo deep also wrong +> end +> end +> echo done +done +> exit +halted +Execution halted. +[exit 0] diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 4e4e9be..a116723 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -24,6 +24,7 @@ Press.sbx 872 Mode.sbx 48 Crash.sbx 632 vars.script 50 +blocks.script 343 tune.sbx 306 notes.txt 21 Apps @@ -36,7 +37,7 @@ outer.script 376 inner.script 44 loop.script 35 crossed.txt 560 -23 files, 1 directory +24 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index e53044c..6e24375 100644 --- a/Tests/expected/cosmosDrives.out +++ b/Tests/expected/cosmosDrives.out @@ -14,6 +14,7 @@ Press.sbx 872 Mode.sbx 48 Crash.sbx 632 vars.script 50 +blocks.script 343 tune.sbx 306 notes.txt 21 Apps @@ -25,7 +26,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -22 files, 1 directory +23 files, 1 directory > drive 1 > dir other.txt 28 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index 8ddfb64..2e7d861 100644 --- a/Tests/expected/cosmosFault.out +++ b/Tests/expected/cosmosFault.out @@ -31,6 +31,7 @@ Press.sbx 872 Mode.sbx 48 Crash.sbx 632 vars.script 50 +blocks.script 343 tune.sbx 306 notes.txt 21 Apps @@ -42,7 +43,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -22 files, 1 directory +23 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index d6846da..618e1a7 100644 --- a/Tests/expected/cosmosGrid.out +++ b/Tests/expected/cosmosGrid.out @@ -21,6 +21,7 @@ Press.sbx 872 Mode.sbx 48 Crash.sbx 632 vars.script 50 +blocks.script 343 tune.sbx 306 notes.txt 21 Apps @@ -32,7 +33,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -22 files, 1 directory +23 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index 004ebdb..de88c48 100644 --- a/Tests/expected/cosmosMonitor.out +++ b/Tests/expected/cosmosMonitor.out @@ -107,6 +107,7 @@ Press.sbx 872 Mode.sbx 48 Crash.sbx 632 vars.script 50 +blocks.script 343 tune.sbx 306 notes.txt 21 Apps @@ -118,7 +119,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -22 files, 1 directory +23 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index 075c64b..e607ba2 100644 --- a/Tests/expected/cosmosMonitorRun.out +++ b/Tests/expected/cosmosMonitorRun.out @@ -26,6 +26,7 @@ Press.sbx 872 Mode.sbx 48 Crash.sbx 632 vars.script 50 +blocks.script 343 tune.sbx 306 notes.txt 21 Apps @@ -37,7 +38,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -22 files, 1 directory +23 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 94a0543..4d6e679 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -14,6 +14,7 @@ Press.sbx 872 Mode.sbx 48 Crash.sbx 632 vars.script 50 +blocks.script 343 tune.sbx 306 notes.txt 21 Apps @@ -25,7 +26,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -22 files, 1 directory +23 files, 1 directory > load load what? > load nosuch.sbx diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index 761c7ff..0c5317a 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -12,6 +12,7 @@ Press.sbx 872 Mode.sbx 48 Crash.sbx 632 vars.script 50 +blocks.script 343 tune.sbx 306 notes.txt 21 Apps @@ -23,7 +24,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -22 files, 1 directory +23 files, 1 directory > load Say.sbx loaded, starting at 5000 > run the disk took its time diff --git a/Tests/input/cosmosBlocks.in b/Tests/input/cosmosBlocks.in new file mode 100644 index 0000000..2d3536b --- /dev/null +++ b/Tests/input/cosmosBlocks.in @@ -0,0 +1,16 @@ +set a hello +if same $a hello +echo taken +else +echo not taken +end +if load nosuch.sbx +echo loaded +else +echo did not +end +else +end +if +do blocks.script +exit diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 53a1dd2..7d784dd 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -132,7 +132,12 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/ # A script that names something nothing was ever set to. Stop on failure is what says the # expansion really did fail rather than merely complaining: the line after it must not run. printf '#! script\necho before\necho $nosuchname\necho after\n' > vars.script +# A script with blocks in it, because that is where they are actually for. It nests, it +# takes the second branch of an outer if, and the branch nobody takes mentions a name that +# was never set - which must not be an error, because nothing there is being run. +printf '#! script\nset colour red\nif same $colour red\n echo it is red\n if same $colour blue\n echo and blue\n else\n echo but not blue\n end\nelse\n echo $neverSetAnywhere\nend\nif same $colour blue\n echo wrong\nelse\n echo right\nend\nif same $colour blue\n if same $colour red\n echo deep wrong\n else\n echo deep also wrong\n end\nend\necho done\n' > blocks.script "$TOOL" put "$DISKS/cosmos.img" vars.script >/dev/null +"$TOOL" put "$DISKS/cosmos.img" blocks.script >/dev/null # tune.sbx, which used to be a boot image and is now a program like any other. It brings a # vector of its own - the screen's, so that it has a beat to play to - which makes it the # second thing here that the version two format exists for. diff --git a/Tests/manifest b/Tests/manifest index 02a1d38..a22ec8a 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -425,6 +425,19 @@ cosmosVars | CosmOS/Source/cosmos.asm | run | cosmosVar # Break is what makes it visible, because it prints the registers. The two dumps have to # agree, and the failures between them are what would move it. cosmosStack | CosmOS/Source/cosmos.asm | run | cosmosStack.in | - | disks/cosmos.img +# Lines that are only run sometimes. "if" takes a COMMAND and what follows runs only if that +# command worked - the Bourne shell's answer, and 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 in LineFailed. +# +# AN if INSIDE A BRANCH NOBODY IS TAKING IS NOT A QUESTION. Its else must not run either, so +# it is pushed as a block that neither branch of can be taken - which is why a block has two +# kinds of not-running rather than one, and why nesting needs no looking down the stack. +# +# THE BRANCH NOBODY TAKES IS NOT EVEN LOOKED AT. The script names a variable that was never +# set, inside the branch that is not taken, and that must not be an error - so the skipping +# happens before the names are filled in, and a line nobody is running cannot fail. +cosmosBlocks | CosmOS/Source/cosmos.asm | run | cosmosBlocks.in | - | disks/cosmos.img # The same editing offered to a PROGRAM, through osReadLine. Edit reads its lines that way, # so a word typed with two letters the wrong way round is put right without starting the line # again - which is the whole of what A4 buys.