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
+33
View File
@@ -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. |
+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:
+1 -1
View File
@@ -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.
+54
View File
@@ -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]
+2 -1
View File
@@ -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 <dir>
@@ -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.
+2 -1
View File
@@ -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 <dir>
@@ -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
+2 -1
View File
@@ -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 <dir>
@@ -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.
+2 -1
View File
@@ -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 <dir>
@@ -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.
+2 -1
View File
@@ -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 <dir>
@@ -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.
+2 -1
View File
@@ -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 <dir>
@@ -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.
+2 -1
View File
@@ -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 <dir>
@@ -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
+2 -1
View File
@@ -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 <dir>
@@ -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
+16
View File
@@ -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
+5
View File
@@ -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.
+13
View File
@@ -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.