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:
Anachronaut
2026-09-01 20:33:33 -04:00
co-authored by Claude Opus 5
parent 4b109f704c
commit 16f8232a35
15 changed files with 523 additions and 15 deletions
+385 -6
View File
@@ -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: