Directories were already blue. Something the shell will run is green now, and it is asked by LOOKING rather than looked up. This was priced once as needing a runnable bit in the directory entry, set by the filesystem from a list of magic numbers it would have to be taught - declined twice, for putting format knowledge in the filesystem and giving two implementations a registry to keep in step. It costs nothing of the sort any more. The shell decides what to run by reading a file's first block, so "will this run" is a question with an answer already, and this asks the same one the shell would: SBEX for a program, "#!" for a script. Nothing is written down and nothing has to agree about anything. The price is a block read per file, which is exactly what the bit existed to avoid: a listing of thirty seven files went from 260,593 cycles to 635,321. It is paid in ls and not in dir on purpose - dir is the listing you audit and is built into the shell, this is the one you read and was loaded off the disk anyway. The fast one stays fast. plain.script is the proof on the test disk: it sits among a dozen scripts that are green and is not one, because it is the fixture with no shebang and the shell will not start it. The extension is decoration and the colour is the truth, which is the whole of what running by content means, finally visible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
459 lines
9.9 KiB
NASM
459 lines
9.9 KiB
NASM
; A listing laid out to be read.
|
|
;
|
|
; > ls
|
|
; Copy.sbx Say.sbx Where.sbx
|
|
; Walk.sbx Lander where.sh
|
|
;
|
|
; > ls /Apps
|
|
;
|
|
; dir says what is there, one line each, with sizes and a tally of the disk. This says the
|
|
; names and nothing else, in columns, which is what you want ninety times in a hundred - and
|
|
; the two are separate programs rather than one with a switch because they answer different
|
|
; questions and neither answer is a worse version of the other.
|
|
;
|
|
; ---- Why this is a program and dir is not ----
|
|
;
|
|
; dir is built into the shell, and that is what makes it the one to trust when the disk is
|
|
; the thing being doubted: it is already in memory. This has to be loaded FROM the disk, so it
|
|
; cannot list a disk too broken to load it. The daily driver and the diagnostic, and it is
|
|
; worth having both.
|
|
;
|
|
; ---- Two passes, and no buffer at all ----
|
|
;
|
|
; Columns need the longest name before the first line can be printed, which usually means
|
|
; holding every name in memory - twenty four bytes each, and this format allows 1,024 entries
|
|
; on a disk. So instead the directory is walked TWICE: once to find the longest name and count
|
|
; them, once to print. A walk costs a read of each directory block and the disk buffer is
|
|
; already there, where a buffer for the names would be memory this program has to have whether
|
|
; the directory is large or not.
|
|
;
|
|
; ---- Across and then down, rather than down and then across ----
|
|
;
|
|
; Real listings go down the columns, so that names next to each other alphabetically are next
|
|
; to each other on the screen. THAT IS A REASON THAT DEPENDS ON SORTING, and nothing here
|
|
; sorts: entries come back in the order the directory holds them, which is the order they were
|
|
; made. With the order arbitrary, down-and-across buys nothing and costs a division to work
|
|
; out how many rows there are - on a machine that cannot divide.
|
|
;
|
|
; So this goes across. If sorting ever arrives, that is the moment to change it, and this
|
|
; comment is the reason why.
|
|
|
|
#Include services.asm
|
|
|
|
#Program
|
|
|
|
#Base 0x5000
|
|
|
|
start:
|
|
; A directory to list, if one was named. The shell puts the working directory back when a
|
|
; program exits, so this can walk off somewhere and not tidy up after itself.
|
|
SETD.0 Given
|
|
INIB 0d64
|
|
SWI osArgument
|
|
SETD.0 Given
|
|
LDA.0
|
|
BRA lsHere
|
|
SWI osChangeDir
|
|
BNQ lsNoSuchPlace
|
|
|
|
lsHere:
|
|
; ---- Pass one: how wide the widest of them is ----
|
|
RSTA
|
|
SETD.0 Longest
|
|
STA.0
|
|
SETD.0 Seen
|
|
STA.0
|
|
|
|
SETD.0 Name
|
|
INIB 0d24
|
|
SWI osDirFirst
|
|
BRI lsMeasure
|
|
|
|
lsMeasureNext:
|
|
SETD.0 Name
|
|
INIB 0d24
|
|
SWI osDirNext
|
|
|
|
lsMeasure:
|
|
MVQA
|
|
SETD.0 Kind
|
|
STA.0
|
|
INIB 0xFF
|
|
CCF
|
|
SUB
|
|
BRQ lsMeasured
|
|
|
|
SETD.0 Seen
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
|
|
CALL nameLength
|
|
MVQA
|
|
|
|
; A directory wears a separator, which is a character of width like any other.
|
|
SETD.0 Kind
|
|
LDB.0
|
|
DECB
|
|
BNB lsMeasureWidth
|
|
INCA
|
|
|
|
lsMeasureWidth:
|
|
SETD.0 Longest
|
|
LDB.0
|
|
CCF
|
|
SUB
|
|
BRC lsMeasureKeep ; Borrowed, so this one is not the longest.
|
|
STA.0
|
|
|
|
lsMeasureKeep:
|
|
BRI lsMeasureNext
|
|
|
|
lsMeasured:
|
|
; Nothing at all is said with nothing at all, which is what an empty directory looks like.
|
|
SETD.0 Seen
|
|
LDA.0
|
|
BRA lsDone
|
|
|
|
; ---- How many of them fit across ----
|
|
;
|
|
; Two spaces between columns, so that names never touch. Worked out by taking the cell width
|
|
; off the screen width until there is not room for another, because this machine cannot
|
|
; divide and the answer is never more than a handful.
|
|
SETD.0 Longest
|
|
LDA.0
|
|
INCA
|
|
INCA
|
|
SETD.0 Cell
|
|
STA.0
|
|
|
|
INA 0x32
|
|
SETD.1 Across
|
|
STA.1 ; What is left of the line, counted down.
|
|
RSTA
|
|
SETD.1 Columns
|
|
STA.1
|
|
|
|
lsFitting:
|
|
SETD.1 Across
|
|
LDA.1
|
|
SETD.1 Cell
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BRC lsFitted ; Borrowed, so there is not room for another column.
|
|
MVQA
|
|
SETD.1 Across
|
|
STA.1
|
|
SETD.1 Columns
|
|
LDA.1
|
|
INCA
|
|
STA.1
|
|
BRI lsFitting
|
|
|
|
lsFitted:
|
|
; A name wider than the screen still gets a column of its own. One that runs over is better
|
|
; than one that is not shown.
|
|
SETD.0 Columns
|
|
LDA.0
|
|
BNA lsPrint
|
|
INIA 0d1
|
|
STA.0
|
|
|
|
lsPrint:
|
|
RSTA
|
|
SETD.0 At
|
|
STA.0
|
|
|
|
SETD.0 Name
|
|
INIB 0d24
|
|
SWI osDirFirst
|
|
BRI lsSay
|
|
|
|
lsSayNext:
|
|
SETD.0 Name
|
|
INIB 0d24
|
|
SWI osDirNext
|
|
|
|
lsSay:
|
|
MVQA
|
|
SETD.0 Kind
|
|
STA.0
|
|
INIB 0xFF
|
|
CCF
|
|
SUB
|
|
BRQ lsLastLine
|
|
|
|
; ---- The cell the last name started, finished now rather than then ----
|
|
;
|
|
; Padding AFTER a name puts spaces at the end of every line that does not fill its last
|
|
; column, and trailing whitespace on a line is the kind of thing that is invisible until
|
|
; something else reads the output. Padding before the next name instead means the spaces
|
|
; only ever go between two things, and a line ends on the last character of a name.
|
|
SETD.0 At
|
|
LDA.0
|
|
BRA lsColour ; First on its line, so there is nothing to finish.
|
|
CALL padOut
|
|
|
|
lsColour:
|
|
; ---- What it is, said in colour ----
|
|
;
|
|
; The attribute reaches a terminal as well as the screen now, so this is one mechanism and
|
|
; not two. A directory in blue, something that will run in green, an unfinished save in red,
|
|
; and everything else plain.
|
|
SETD.0 Kind
|
|
LDA.0
|
|
BRA lsFileInk
|
|
DECA
|
|
BRA lsBlue
|
|
INIA 0d1 ; An unfinished save, which is worth looking at.
|
|
BRI lsInk
|
|
lsBlue:
|
|
INIA 0d4
|
|
BRI lsInk
|
|
|
|
lsFileInk:
|
|
; ---- Whether it will run, asked by looking ----
|
|
;
|
|
; This was priced once as needing a RUNNABLE BIT in the directory entry, set by the
|
|
; filesystem from a list of magic numbers it would have to be taught - which was declined,
|
|
; twice, for putting format knowledge in the filesystem and giving two implementations a
|
|
; registry to keep in step.
|
|
;
|
|
; It costs nothing of the sort now. The shell decides what to run by READING the first block
|
|
; of a file, so "will this run" is a question with an answer already, and asking it is the
|
|
; same question the shell would ask. Nothing new is written down and nothing has to agree
|
|
; about anything: if the shell learns a third kind of runnable file, this is wrong until
|
|
; somebody adds two lines, and wrong in the direction of a listing that is less colourful.
|
|
;
|
|
; The price is a block read per file, which is what the bit existed to avoid. It is paid
|
|
; here rather than in dir on purpose: dir is the listing you audit and is built into the
|
|
; shell, and this is the one you read and is a program that was loaded off the disk anyway.
|
|
CALL runnable
|
|
BNQ lsPlain
|
|
INIA 0d2
|
|
BRI lsInk
|
|
lsPlain:
|
|
RSTA
|
|
lsInk:
|
|
OUTA 0x06
|
|
|
|
SETD.0 Name
|
|
SWI osPrintString
|
|
CALL nameLength
|
|
MVQA
|
|
|
|
SETD.0 Kind
|
|
LDB.0
|
|
DECB
|
|
BNB lsWidth
|
|
PSHA
|
|
SETD.0 Slash
|
|
SWI osPrintString
|
|
POPA
|
|
INCA
|
|
|
|
lsWidth:
|
|
; How much of the cell this name took, for whoever finishes it.
|
|
SETD.0 Used
|
|
STA.0
|
|
|
|
; Back to plain before anything else is written, so that a scheme with paper in it would
|
|
; not paint the gap between the columns.
|
|
RSTA
|
|
OUTA 0x06
|
|
|
|
SETD.0 At
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
SETD.0 Columns
|
|
LDB.0
|
|
CCF
|
|
SUB
|
|
BNQ lsSayNext ; Room for another on this line.
|
|
|
|
RSTA
|
|
SETD.0 At
|
|
STA.0
|
|
CALL newLine
|
|
BRI lsSayNext
|
|
|
|
lsLastLine:
|
|
; A line with something on it needs ending. One that ended on its last column does not, and
|
|
; a blank line under a listing looks like a listing with a gap in it.
|
|
SETD.0 At
|
|
LDA.0
|
|
BRA lsDone
|
|
CALL newLine
|
|
|
|
lsDone:
|
|
RSTA
|
|
SWI osExit
|
|
|
|
lsNoSuchPlace:
|
|
SETD.0 NoSuchPlace
|
|
SWI osPrintString
|
|
SETD.0 Given
|
|
SWI osPrintString
|
|
CALL newLine
|
|
INIA 0x01
|
|
SWI osExit
|
|
|
|
; ---- How wide the name in the buffer is ----
|
|
;
|
|
; Q COMES BACK THE COUNT, not A. A RET puts A back the way the caller had it, so a subroutine
|
|
; that answers in A answers with nothing - and this one did, which made every gap between the
|
|
; columns the same width because the padding was subtracting whatever A happened to hold.
|
|
; Q is the ALU's output and nothing puts it back, which is why every answer on this machine
|
|
; comes home in it.
|
|
nameLength:
|
|
SETD.0 Name
|
|
RSTA
|
|
lengthNext:
|
|
LDB.0
|
|
BRB lengthDone
|
|
INCA
|
|
INCD.0
|
|
BRI lengthNext
|
|
lengthDone:
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; ---- Will the shell start this? ----
|
|
;
|
|
; Q is zero if it will. The same two things the shell itself looks for in a first block: SBEX
|
|
; for a program, "#!" for a script. A file too short to have a first block at all cannot be
|
|
; either, and osFileBlock saying no is that answer arriving for free.
|
|
runnable:
|
|
SETD.0 Name
|
|
SETD.1 Block
|
|
RSTA
|
|
RSTB
|
|
SWI osFileBlock
|
|
BNQ runnableNo
|
|
|
|
; The shebang first, because two bytes settle it.
|
|
SETD.0 Block
|
|
LDA.0
|
|
INIB 0d35 ; '#'
|
|
CCF
|
|
SUB
|
|
BNQ runnableExec
|
|
INCD.0
|
|
LDA.0
|
|
INIB 0d33 ; '!'
|
|
CCF
|
|
SUB
|
|
BRQ runnableYes
|
|
|
|
runnableExec:
|
|
SETD.0 Block
|
|
SETD.1 ExecText
|
|
INIA 0d4
|
|
SETD.2 Left
|
|
STA.2
|
|
runnableSame:
|
|
LDA.0
|
|
LDB.1
|
|
CCF
|
|
SUB
|
|
BNQ runnableNo
|
|
INCD.0
|
|
INCD.1
|
|
SETD.2 Left
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BNA runnableSame
|
|
|
|
runnableYes:
|
|
RSTA
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
runnableNo:
|
|
INIA 0x01
|
|
RSTB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; A is how much of the cell the name took. Fills the rest of it with spaces.
|
|
padOut:
|
|
; The cell less what the name took, and IN THAT ORDER. Written the other way round first,
|
|
; which borrowed on every name that was not the longest - so every one of them took the
|
|
; "wider than its cell" path below and the whole listing came out separated by one space.
|
|
SETD.0 Cell
|
|
LDA.0
|
|
SETD.0 Used
|
|
LDB.0
|
|
CCF
|
|
SUB
|
|
BRC padDone ; Wider than its cell, so one space keeps the names apart.
|
|
MVQA
|
|
BRA padOne
|
|
padLoop:
|
|
PSHA
|
|
SETD.0 Space
|
|
SWI osPrintString
|
|
POPA
|
|
DECA
|
|
BNA padLoop
|
|
RET
|
|
padOne:
|
|
padDone:
|
|
SETD.0 Space
|
|
SWI osPrintString
|
|
RET
|
|
|
|
newLine:
|
|
SETD.0 NewLine
|
|
SWI osPrintString
|
|
RET
|
|
|
|
#Data
|
|
|
|
#Base 0x3000
|
|
|
|
ExecText:
|
|
"SBEX"
|
|
Slash:
|
|
"/"
|
|
Space:
|
|
" "
|
|
NoSuchPlace:
|
|
"there is no such directory: "
|
|
NewLine:
|
|
0x0A 0x00
|
|
|
|
Kind:
|
|
0x00
|
|
Longest:
|
|
0x00
|
|
Cell:
|
|
0x00
|
|
Columns:
|
|
0x00
|
|
Across:
|
|
0x00
|
|
At:
|
|
0x00
|
|
Seen:
|
|
0x00
|
|
Used:
|
|
0x00
|
|
Left:
|
|
0x00
|
|
Name:
|
|
#Reserve 0d24
|
|
Given:
|
|
#Reserve 0d64
|
|
; A whole block, because osFileBlock writes one whether the file fills it or not.
|
|
Block:
|
|
#Reserve 0d256
|