Tab lists its matches in columns, and says what it can in colour

Tab's listing was six instructions: print the name, print two spaces. It
was the crown of this shell for a while and looks plain next to ls.

Columns cost NOTHING EXTRA. Tab already walks its candidates twice - once
to find the answer, once to show the matches, and tabRunSources exists
because those are the same walk asked two questions. The longest match is
counted on the first, which already visits every one of them, so the
listing needs no walk of its own to know how wide a column should be.
Padding goes before a name rather than after, so a row ends on a name.

Directories are blue and the shell's own commands are green. Both are
free: Tab appends the separator itself, and a built-in is not a file at
all - which is also the only way anybody could know it will run.

A FILE IS NOT COLOURED, and that is a decision. Whether a file will run
is a read of its first block, which is what ls does and what makes ls
cost twice what it otherwise would. Here it would be worse than slow:
candidates are offered from inside a directory walk, and looking a file
up would overwrite the very fields holding the walk's own position. Doing
it safely means holding every candidate name in memory, which is a buffer
the shell would carry whether anybody pressed Tab or not - and ls is one
keystroke away. A Tab press already costs about 200,000 cycles, so the
read was not the objection.

---- And the machine could no longer build itself ----

Found by make test, not by reading. The native assembler ran out of room
for label names on cosmos.asm: 16,758 bytes against 16,384. The index was
1,341 of 1,536 in the same breath.

Which is scratch.asm's own warning happening a second time - "two
ceilings a hundred bytes apart look like one ceiling until the first is
lifted" - so both were raised, out of the seventeen kilobyte page that
file deliberately left unclaimed against exactly this. Names to 26,624
and the index to 2,048, both left about a third clear, with 1,792 bytes
still unclaimed for the same reason.

A name is thirteen bytes on average and an index entry is four, so the
arena will always be the one that speaks first. That is now written down
where the two numbers are.

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-06 20:18:56 -04:00
co-authored by Claude Opus 5
parent eaeb473176
commit 563bc20a75
9 changed files with 263 additions and 26 deletions
+208 -3
View File
@@ -1739,6 +1739,8 @@ tabIsFirst:
STA.1
SETD.1 TabBestLen
STA.1
SETD.1 TabLongest
STA.1
; A path in the word says which directory to look in, and narrows what is being matched to
; the part after the last separator.
@@ -1929,6 +1931,16 @@ tabOfferTake:
LDA.1
INCA
STA.1
; ---- How wide the widest of them is, counted on the pass that is already here ----
;
; Listing wants it and listing happens on the SECOND pass, by which time this one is over.
; Working it out there would mean a third walk of everything; working it out here costs a
; walk of one name, and only of names that matched.
CALL tabWidest
SETD.1 TabCount
LDA.1
INIB 0d1
XOR
BRQ tabOfferFirst
@@ -1939,12 +1951,62 @@ tabOfferFirst:
BRI tabOfferSkip
tabOfferPrint:
; ---- The cell the last one started, finished now ----
;
; Padding before rather than after, so a line ends on the last character of a name instead
; of on a run of spaces. The same reason ls does it that way.
SETD.1 TabAcross
LDA.1
BRA tabOfferInk
CALL tabPad
tabOfferInk:
; A directory in blue and a built-in command in green, which are the two things that can be
; known for nothing. Whether a FILE will run cannot: that is a read of its first block, and
; this is a keystroke.
SETD.1 TabKind
LDA.1
BRA tabOfferSay
DECA
BRA tabOfferBlue
INIA 0d2
BRI tabOfferInked
tabOfferBlue:
INIA 0d4
BRI tabOfferInked
tabOfferSay:
RSTA
tabOfferInked:
OUTA 0x06
SETD.1 TabStart
LDD.0.1
CALL printString
INIA 0x20
OUTA 0x00
OUTA 0x00 ; Two spaces between them, and A already holds one.
RSTA
OUTA 0x06
; How much of the cell it took, for whoever finishes it, and one more across the line.
SETD.1 TabStart
LDD.0.1
CALL tabTextLength
MVQA
SETD.1 TabUsed
STA.1
SETD.1 TabAcross
LDA.1
INCA
STA.1
SETD.1 TabColumns
LDB.1
CCF
SUB
BNQ tabOfferSkip
RSTA
SETD.1 TabAcross
STA.1
CALL newLine
tabOfferSkip:
; Past the end of this candidate, from wherever the comparison stopped.
@@ -2258,6 +2320,9 @@ tabEntry:
; same question the same way round.
; A directory, and the separator goes on the end of it.
INIA 0d1
SETD.1 TabKind
STA.1
SETD.3 TabCandidate
CALL tabEnd
INIA 0x2F
@@ -2267,6 +2332,10 @@ tabEntry:
STA.3
tabEntryFile:
RSTA
SETD.1 TabKind
STA.1
; A file, offered under the name it actually has - so nothing happens to it here at all.
; A first word used to be offered with ".sbx" taken off, and anything without that ending
; not offered, which was right while the extension was what made a file reachable by name.
@@ -2323,11 +2392,73 @@ tabNameNext:
BRA tabNamesDone
DECA
STA.1
INIA 0d2
SETD.1 TabKind
STA.1 ; A command of the shell's own, which no disk read could say.
CALL tabOffer
BRI tabNameNext
tabNamesDone:
RET
; DP0 names a string. Q is how long it is. Q rather than A, because a RET puts A back - which
; is the oldest trap on this machine and cost an afternoon in ls.
tabTextLength:
RSTA
tabTextNext:
LDB.0
BRB tabTextDone
INCA
INCD.0
BRI tabTextNext
tabTextDone:
RSTB
CCF
ADD
RET
; The candidate at TabStart against the longest seen so far, called on the pass that finds the
; answer - so listing costs no walk of its own to know how wide a column should be.
tabWidest:
SETD.1 TabStart
LDD.0.1
CALL tabTextLength
MVQA
SETD.1 TabLongest
LDB.1
CCF
SUB
BRC tabWidestKept ; Borrowed, so this one is not the longest.
STA.1
tabWidestKept:
RET
; Fills out the cell the last name started.
tabPad:
SETD.1 TabCell
LDA.1
SETD.1 TabUsed
LDB.1
CCF
SUB
BRC tabPadOne ; Wider than its cell, so one space keeps them apart.
MVQA
BRA tabPadOne
SETD.1 TabPadLeft
STA.1
tabPadNext:
INIA 0x20
OUTA 0x00
SETD.1 TabPadLeft
LDA.1
DECA
STA.1
BNA tabPadNext
RET
tabPadOne:
INIA 0x20
OUTA 0x00
RET
; ---- Showing them when they cannot be narrowed further ----
;
; The line is reprinted underneath afterwards, and where it now BEGINS is asked rather than
@@ -2335,6 +2466,52 @@ tabNamesDone:
; editor's idea of where things are hangs off those two registers.
tabList:
CALL newLine
; ---- How many of them fit across ----
;
; The longest was counted on the pass that found the answer, so this is arithmetic and not
; another walk. Two spaces between columns, and the screen says how wide it is - the same
; sum ls does, for the same reason and with the same answer.
SETD.1 TabLongest
LDA.1
INCA
INCA
SETD.1 TabCell
STA.1
INA 0x32
SETD.1 TabRoom
STA.1
RSTA
SETD.1 TabColumns
STA.1
tabFitting:
SETD.1 TabRoom
LDA.1
SETD.1 TabCell
LDB.1
CCF
SUB
BRC tabFitted
MVQA
SETD.1 TabRoom
STA.1
SETD.1 TabColumns
LDA.1
INCA
STA.1
BRI tabFitting
tabFitted:
; A candidate wider than the screen still gets a column to itself.
SETD.1 TabColumns
LDA.1
BNA tabListing
INIA 0d1
STA.1
tabListing:
RSTA
SETD.1 TabAcross
STA.1
INIA 0x01
SETD.1 TabShowing
STA.1
@@ -2342,7 +2519,14 @@ tabList:
RSTA
SETD.1 TabShowing
STA.1
; A row with something in it needs ending. One that filled its last column does not, and a
; blank line under a listing looks like a listing with a gap in it.
SETD.1 TabAcross
LDA.1
BRA tabListed
CALL newLine
tabListed:
CALL sayPrompt
INA 0x03
@@ -8728,6 +8912,27 @@ TabCopyAt:
0x00
TabFirst:
0x00
; ---- Laying the matches out in columns ----
;
; What kind of thing the candidate being offered is, how wide the widest of them was, and how
; far along a row the listing has got. The kind is set where the candidate comes FROM, since
; that is the only place that knows: 0 a file, 1 a directory, 2 one of the shell's own words.
TabKind:
0x00
TabLongest:
0x00
TabCell:
0x00
TabColumns:
0x00
TabRoom:
0x00
TabAcross:
0x00
TabUsed:
0x00
TabPadLeft:
0x00
; Whether the walk is looking for the answer or showing what the answers were.
TabShowing:
0x00