Names for things

"set apps /Apps", and then "$apps" anywhere on a later line stands for it. A
name stops where a name stops - letters and digits - so it composes into a path
without anything having to be quoted, which is the whole reason a script would
want one.

THE SUBSTITUTION HAPPENS ON EVERY LINE THE SHELL IS ABOUT TO RUN, typed or read
out of a file, so the two behave the same and no command below has to know that
variables exist. Same shape as the line editing: one place the whole system
already flows through, rather than a decision made twenty times.

A NAME NOTHING WAS SET TO DOES NOT RUN THE LINE. Every other shell expands it to
nothing, and that is the wrong answer here: a mistyped name would quietly become
an empty path, which is the class of silent wrong answer the rest of this system
spends its effort refusing. It says so and the line counts as failed, which
stops a script - and the test proves that by running one, where the line after
it must not appear. Somebody who wants an empty value writes "set name" and gets
one, so the escape hatch exists and has to be asked for.

A NAME TOO LONG IS AN ERROR RATHER THAN A SHORTER NAME. Cutting it off at
fifteen characters was the first version, and it is the same fault wearing a
different coat: two names differing only after the fifteenth would be one
variable, and the complaint about a missing one printed a word nobody typed.

Eight slots of sixty four bytes - sixteen of name, forty eight of value - and
sixty four rather than eighty because A and B are a sixteen bit shift register,
so two rotations turn a slot number into its offset. The same trick the history
uses, and the reason neither needs a multiply this machine has not got.

TWO THINGS I GOT WRONG AND ONE I FOUND:

doSetVar ended in RET. It is BRANCHED to from the dispatch, not called, so that
RET went wherever the Stack happened to point - the same fault that formatted a
disk last week, in a command written three days after the rule was named. The
new lint rule does not catch this shape: it fires on falling INTO a subroutine,
not on a branch target that ends like one.

And a test of the expansion's answer, which is dead code: commandFailed does not
return. It marks the line and branches to the prompt, the way every failure in
this shell is reported, so the only way out of the expansion is the one where it
worked.

Which turned up a real leak, measured and not yet fixed: every failure that goes
through commandFailed abandons the frames between the prompt and the call. SP
goes from FFFD to FE6D over twenty of them, twenty bytes each. Its own commit.

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 19:23:55 -04:00
co-authored by Claude Opus 5
parent 3c76934a9a
commit a8707f29f0
17 changed files with 637 additions and 16 deletions
+32
View File
@@ -116,6 +116,37 @@ 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.
## Names For Things:
`set name value` writes one down, and `$name` anywhere on a later line stands for it.
```text
> set apps /Apps
> echo $apps/Copy.sbx
/Apps/Copy.sbx
```
A name stops where a name stops - letters and digits - so it composes into a path without
anything having to be quoted. Eight of them can be set at once, names up to fifteen
characters and values up to forty seven.
**The substitution happens on every line the shell is about to run**, typed or read from a
file, so scripts and typing behave the same and no command has to know that variables exist.
`set` on its own says what is written down.
**A name nothing was ever set to does not run the line.** It says so, and the line counts as
failed - which stops a script, the same as any other failure. Every other shell expands an
unset name to nothing, and that is the wrong answer here: a mistyped name would quietly
become an empty path, which is the kind of silent wrong answer the rest of this system spends
its effort refusing. Somebody who genuinely wants an empty value writes `set name` with
nothing after it and gets one, so the escape hatch exists and has to be asked for.
A name longer than fifteen characters is an error for the same reason. Cutting it short would
make two different names one variable and would put a word the person never typed into the
message about it.
A `$` with nothing name-like after it is just a `$`.
## Scripts:
`do <file>` runs the lines in a file as though somebody had typed them. Every command works
@@ -241,6 +272,7 @@ CosmOS currently provides these built-in commands:
| `<name> [words]` | Any word the shell does not recognise is looked for on the disk as `<name>.sbx`, and loaded and started if it is there. |
| `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. |
| `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. |
+521 -1
View File
@@ -211,6 +211,19 @@ promptWhere:
RSTA
STA.1
; ---- Names filled in before anybody reads the line ----
;
; Here rather than in the script reader, so that a typed line and a line out of a file
; behave the same and no command below has to know that variables exist. A line that names
; something nothing was ever set to does not run at all: it says so and counts as failed,
; which is what makes a mistyped name a complaint instead of an empty path.
;
; THERE IS NOTHING TO TEST AFTERWARDS, and a test was written here before that was noticed:
; 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.
CALL varExpand
SETD.0 CommandLine
CALL textSplit
@@ -279,6 +292,11 @@ promptWhere:
CALL textSame
BRQ doScript
SETD.0 CommandLine
SETD.1 SetVarName
CALL textSame
BRQ doSetVar
SETD.0 CommandLine
SETD.1 HelpName
CALL textSame
@@ -2284,6 +2302,465 @@ tabList:
CALL editRedraw
BRI editKey
; ---- Things a line can be given a name for ----
;
; Eight of them, so a script can hold a path or a half written command and compose the rest
; around it later. Each is one record of sixty four bytes: sixteen of name, then forty eight
; of value. SIXTY FOUR RATHER THAN EIGHTY, because A and B are a sixteen bit shift register
; and two rotations right turn a slot number into the offset of its slot - the same trick the
; history uses, and the reason neither of them needs a multiply this machine has not got.
;
; A slot whose name begins with a zero is free. Nothing counts them and nothing has to be
; kept in step.
;
; ---- A name that was never set is an error, and says so ----
;
; Expanding it to nothing is what other shells do and it is the wrong answer here: a mistyped
; name would quietly become an empty path, and everything else in this system spends its
; effort on saying what went wrong. Somebody who genuinely wants an empty value writes
; "set name" with nothing after it and gets one - so the escape hatch exists, and has to be
; asked for rather than arrived at by accident.
; DP3 to variable slot A, which is 0 to 7.
varSlotAt:
SETD.3 VarSlots
RSTB
SHR
SHR ; A and B together are the slot number times sixty four.
DPUW.3
RET
; DP0 names a name. Q is zero if it is there, and DP3 is left on its slot.
varFind:
SETD.1 VarWanted
STD.0.1
RSTA
SETD.1 VarSlot
STA.1
varFindNext:
SETD.1 VarSlot
LDA.1
INIB 0d8
CCF
SUB
BNC varFindNo ; Past the last slot.
LDA.1
CALL varSlotAt
SETD.1 VarWanted
LDD.0.1
LDA.3
BRA varFindStep ; A free slot has no name to be the one wanted.
varFindMatch:
LDA.0
BRA varFindEnded
LDB.3
XOR
BNQ varFindStep
INCD.0
INCD.3
BRI varFindMatch
varFindEnded:
; The same only if the slot's name ends here too. "path" and "pathname" are two names, and
; comparing until the shorter runs out would call them one.
LDA.3
BNA varFindStep
SETD.1 VarSlot
LDA.1
CALL varSlotAt
RSTA
RSTB
CCF
ADD ; Q is zero: found.
RET
varFindStep:
SETD.1 VarSlot
LDA.1
INCA
STA.1
BRI varFindNext
varFindNo:
INIA 0x01
RSTB
CCF
ADD ; Q is one: no such name.
RET
; DP3 names a slot. Moves it on to that slot's value.
varValueOf:
INIA 0d16
DPUA.3
RET
; DP0 names a name and DP1 its value. Q is zero if it was written down.
varSet:
SETD.2 VarKeepValue
STD.1.2 ; The value, while a slot is looked for.
CALL varFind
BRQ varSetInto ; Already there, so it is written over.
; A free one, which is any whose name begins with a zero.
RSTA
SETD.1 VarSlot
STA.1
varSetFree:
SETD.1 VarSlot
LDA.1
INIB 0d8
CCF
SUB
BNC varSetFull
LDA.1
CALL varSlotAt
LDA.3
BRA varSetInto
SETD.1 VarSlot
LDA.1
INCA
STA.1
BRI varSetFree
varSetInto:
; Which slot it is, is in VarSlot either way: varFind leaves the one it found there, and
; the search above leaves the free one it stopped on. So it is worked out again rather than
; carried in a pointer through two calls that would each put it back.
SETD.1 VarSlot
LDA.1
CALL varSlotAt
PSHD.3
POPD.1
INIA 0d15
CALL varCopy ; DP0 is still the name.
SETD.1 VarSlot
LDA.1
CALL varSlotAt
CALL varValueOf
PSHD.3
POPD.1
SETD.2 VarKeepValue
LDD.0.2
INIA 0d47
CALL varCopy
RSTA
RSTB
CCF
ADD ; Q is zero: it is written down.
RET
varSetFull:
INIA 0x01
RSTB
CCF
ADD ; Q is one: there is no room for another.
RET
; DP0 to DP1, at most A characters and then the zero that ends them.
varCopy:
SETD.2 VarRoom
STA.2
varCopyNext:
SETD.2 VarRoom
LDA.2
BRA varCopyDone
DECA
STA.2
LDA.0
BRA varCopyDone
STA.1
INCD.0
INCD.1
BRI varCopyNext
varCopyDone:
RSTA
STA.1
RET
; A holds a character. Q is zero if it is one a name may be made of, which is what decides
; where a name written in the middle of a path stops: "$where/file" is a name and a path.
varNameChar:
INIB 0x30
CCF
SUB
BRC varNameCharNo ; Below a digit.
INIB 0x3A
CCF
SUB
BRC varNameCharYes ; A digit.
INIB 0x41
CCF
SUB
BRC varNameCharNo
INIB 0x5B
CCF
SUB
BRC varNameCharYes ; A capital.
INIB 0x61
CCF
SUB
BRC varNameCharNo
INIB 0x7B
CCF
SUB
BRC varNameCharYes ; A small letter.
varNameCharNo:
INIA 0x01
RSTB
CCF
ADD
RET
varNameCharYes:
RSTA
RSTB
CCF
ADD
RET
; ---- The line, with every name in it replaced by what it stands for ----
;
; Done here, on its way from wherever it was read to whoever runs it, so that a script line
; and a typed line behave the same and no command has to know that variables exist at all.
; The same shape as the line editing: one place the whole system already flows through.
;
; WHERE THE OUTPUT HAS GOT TO IS KEPT IN MEMORY rather than in a pointer. There are four
; pointers and this needs the line being read, the name being gathered, the value being
; copied and somewhere to put each character - and the moment the output position lived in
; one of them, gathering a name reset it to the start of the line.
;
; Q is zero if the line is ready to run.
varExpand:
SETD.0 VarLine
SETD.1 VarPut
STD.0.1
RSTA
SETD.1 VarUsed
STA.1
SETD.0 CommandLine
varExpandNext:
LDA.0
BRA varExpandDone
INIB 0x24
XOR
BRQ varExpandName
CALL varExpandPut
BNQ varExpandLong
INCD.0
BRI varExpandNext
varExpandName:
; Past the marker, and then as much of a name as follows it.
INCD.0
SETD.2 VarName
RSTA
SETD.1 VarNameLen
STA.1
varExpandNameChar:
LDA.0
CALL varNameChar
BNQ varExpandNamed
SETD.1 VarNameLen
LDA.1
INIB 0d15
CCF
SUB
BNC varExpandNameLong ; One more and there would be nowhere to put it.
LDA.0
STA.2
INCD.2
INCD.0
LDA.1
INCA
STA.1
BRI varExpandNameChar
varExpandNamed:
RSTA
STA.2 ; The zero that ends the gathered name.
SETD.1 VarNameLen
LDA.1
BNA varExpandLookUp
; A marker with nothing after it is just a marker, and goes through as one rather than
; being an error about a variable nobody mentioned.
INIA 0x24
CALL varExpandPut
BNQ varExpandLong
BRI varExpandNext
varExpandLookUp:
SETD.1 VarKeepLine
STD.0.1 ; Where the line has got to, across the search.
SETD.0 VarName
CALL varFind
BNQ varExpandNoSuch
CALL varValueOf
SETD.1 VarKeepLine
LDD.0.1
varExpandValue:
LDA.3
BRA varExpandNext
CALL varExpandPut
BNQ varExpandLong
INCD.3
BRI varExpandValue
varExpandDone:
RSTA
CALL varExpandPut ; The zero that ends the built line.
BNQ varExpandLong
; And back where everything downstream looks for it.
SETD.0 VarLine
SETD.1 CommandLine
INIA 0d127
CALL varCopy
RSTA
RSTB
CCF
ADD
RET
; A holds a character. Puts it where the built line has got to. Q is one if there is no room,
; which is a line that grew past what the shell can hold once its names were filled in.
varExpandPut:
SETD.2 VarHold
STA.2
SETD.2 VarUsed
LDA.2
INIB 0d127
CCF
SUB
BNC varExpandNoRoom
SETD.2 VarPut
LDD.1.2
SETD.2 VarHold
LDA.2
STA.1
INCD.1
SETD.2 VarPut
STD.1.2
SETD.2 VarUsed
LDA.2
INCA
STA.2
RSTA
RSTB
CCF
ADD
RET
varExpandNoRoom:
INIA 0x01
RSTB
CCF
ADD
RET
; ---- A name too long to be one is not a shorter name ----
;
; Cutting it off at fifteen characters is what this did, and it is the quiet wrong answer
; this whole feature was built to avoid: two names that differ only after the fifteenth
; character would be ONE variable, and the message about a missing one would name something
; the person never typed.
varExpandNameLong:
SETD.0 VarNameLong
CALL printString
CALL newLine
CALL commandFailed ; Which does not come back.
varExpandLong:
SETD.0 VarTooLong
CALL printString
CALL newLine
CALL commandFailed ; Which does not come back.
varExpandNoSuch:
SETD.0 VarNoSuch
CALL printString
SETD.0 VarName
CALL printString
CALL newLine
CALL commandFailed ; Which does not come back.
; ---- set ----
;
; "set name value" writes one down, "set name" gives it an empty value on purpose, and "set"
; on its own says what is written down already - because a name that fails loudly when it was
; never set needs somewhere to go and look.
doSetVar:
SETD.1 TextRest
LDD.0.1
LDA.0
BRA setVarList ; Nothing after the word, so this is asking what is set.
; DP0 is still on the rest of the line - reading a byte through a pointer does not move it -
; so the name starts where it already points.
SETD.1 SetVarWhich
STD.0.1
CALL textSplit
SETD.0 SetVarWhich
LDD.0.0
SETD.1 TextRest
LDD.1.1
CALL varSet
BNQ setVarFull
BRI prompt
setVarFull:
SETD.0 SetVarNoRoom
CALL printString
CALL newLine
CALL commandFailed
BRI prompt
setVarList:
RSTA
SETD.1 VarSlot
STA.1
setVarListNext:
SETD.1 VarSlot
LDA.1
INIB 0d8
CCF
SUB
BNC setVarListDone
LDA.1
CALL varSlotAt
LDA.3
BRA setVarListStep ; A free slot has nothing to say.
PSHD.3
POPD.0
CALL printString
SETD.0 SetVarIs
CALL printString
SETD.1 VarSlot
LDA.1
CALL varSlotAt
CALL varValueOf
PSHD.3
POPD.0
CALL printString
CALL newLine
setVarListStep:
SETD.1 VarSlot
LDA.1
INCA
STA.1
BRI setVarListNext
setVarListDone:
BRI prompt
; ---- A command that did not work ----
;
; The one place a failure is recorded, so that the thing reading lines out of a file can
@@ -6449,15 +6926,27 @@ ExitName:
"exit"
MonitorName:
"monitor"
SetVarName:
"set"
; How many of them, since a run of strings does not say where it stops.
ShellNameCount:
0d15
0d16
SbxText:
".sbx"
AppsPath:
"/Apps"
VarNoSuch:
"nothing is set called "
VarTooLong:
"that line is too long once its names are filled in"
VarNameLong:
"that name is longer than a name can be"
SetVarNoRoom:
"there is no room for another name"
SetVarIs:
" is "
; 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.
@@ -6837,6 +7326,37 @@ TabWasCwd:
TabWasDrive:
0x00
; ---- What a name stands for ----
;
; Eight records of sixty four bytes: sixteen of name, then forty eight of value.
VarSlots:
#Reserve 0d512
VarSlot:
0x00
VarWanted:
0x00 0x00
VarKeepValue:
0x00 0x00
VarKeepLine:
0x00 0x00
VarRoom:
0x00
VarHold:
0x00
; The line with its names filled in, built here and copied back when it is whole.
VarLine:
#Reserve 0d128
VarPut:
0x00 0x00
VarUsed:
0x00
VarName:
#Reserve 0d16
VarNameLen:
0x00
SetVarWhich:
0x00 0x00
; ---- What the fault screen is saying ----
;
; Whether this cause names an entry as well as itself, and which one. Only the two faults