Lines that are only run sometimes
if, else, end, and same. IF TAKES A COMMAND, which is one rule rather than two and 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 is a question too - "if load Snake.sbx" is a perfectly good one. The shell already had the other half. LineFailed exists because a script stops at the first line that did not work, so every command was already saying whether it had, for a different reason entirely. A BLOCK HAS TWO KINDS OF NOT-RUNNING. One where an else would turn it on, and one where it would not - which is what an if pushes when something above it is already being skipped. That is what makes nesting need no looking down the stack: the top of it says everything. A branch nobody is taking is not even looked at. The skipping happens BEFORE the names are filled in, so a variable mentioned in a branch that is not running is not an error - a line nobody runs must not be able to fail. AND LINES MAY BE INDENTED, which they could not be before there was anything to indent inside. Nobody writes an if inside an if without indenting what is in them, and a leading space used to make the first word empty and match nothing. Found by writing the test script the way anybody would write one. CALL commandFailed became BRI commandFailed in nine places. It never returns - it marks the line and branches to the prompt - so calling it was a lie that cost a Stack frame each time, and fourteen other sites already branched. THE LINT RULE FOUND THIS, three days after I wrote the rule and on my own code: two false positives that were really the linter being right about a CALL that is not one. It does not fix the leak on its own, since a failure inside any called routine still abandons that frame, but it removes the cause of the commonest case and makes the code true. The mechanical edit then left a BRI prompt stranded behind one of them, and the linter caught that too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
4b109f704c
commit
16f8232a35
@@ -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 <file>` | Remove a file from the filesystem and release its blocks. |
|
||||
| `rename <file> <to>` | 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 <command>` | 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 <a> <b>` | 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. |
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user