From 563bc20a75980dbf641d72b39b97d47ebc9c4183 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Sun, 6 Sep 2026 20:18:56 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Programs/CosmOS/Assembler/labels.asm | 19 ++- Programs/CosmOS/Assembler/scratch.asm | 32 ++-- Programs/CosmOS/README.md | 17 +++ Programs/CosmOS/Source/cosmos.asm | 211 +++++++++++++++++++++++++- Tests/expected/cfgFallback.out | 2 +- Tests/expected/cosmosTab.out | 2 +- Tests/expected/onceAsked.out | 2 +- Tests/expected/romBoot.out | 2 +- Tests/expected/selfBoot.out | 2 +- 9 files changed, 263 insertions(+), 26 deletions(-) diff --git a/Programs/CosmOS/Assembler/labels.asm b/Programs/CosmOS/Assembler/labels.asm index 6b9ba35..66cba7c 100644 --- a/Programs/CosmOS/Assembler/labels.asm +++ b/Programs/CosmOS/Assembler/labels.asm @@ -297,18 +297,21 @@ LabEnd: ; How many labels there may be, and how many bytes of name between them. ; ; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to -; build: 555 labels and about 6,800 bytes of name, against CosmOS's 650 and 8,081 - CosmOS is -; the bigger of the two now, and was the smaller when this was written. Running into either +; build: the assembler is about 555 labels, against CosmOS's 1,341 and 16,758 bytes of name +; measured 2026-09-06 - CosmOS is much the bigger of the two now, and was the smaller when +; this was written. Running into either ; limit says so rather than writing past the end of the table, and that is what it did. The buffers live ; above the program rather than inside it - see the scratch map in Asm.asm. -; Fifteen hundred and thirty six names, and sixteen kilobytes to hold them in. Both were -; half that, and the names ran out first: 8,081 bytes of 8,192, which is a hundred and -; eleven - and the thing that ran into it was one ordinary piece of work adding eighteen -; labels. THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is. +; Two thousand and forty eight names, and twenty six kilobytes to hold them in. Both have +; now been doubled twice, and BOTH TIMES THE NAMES RAN OUT FIRST with the index a couple of +; hundred behind them - 8,081 of 8,192 the first time, 16,758 of 16,384 the second. A name +; is thirteen bytes on average and an index entry is four, so the arena will always be the +; one that speaks; raising it alone would buy a few hundred labels and then the other wall. +; THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is. LabLimit: - 0x06 0x00 + 0x08 0x00 LabRoom: - 0x40 0x00 + 0x68 0x00 LabNamed: ": " diff --git a/Programs/CosmOS/Assembler/scratch.asm b/Programs/CosmOS/Assembler/scratch.asm index 4744635..d3c934a 100644 --- a/Programs/CosmOS/Assembler/scratch.asm +++ b/Programs/CosmOS/Assembler/scratch.asm @@ -20,10 +20,10 @@ ; bad enough; a stale number sitting next to the correction is worse, because whichever ; one a reader takes is a coin toss. ; -; 0x5000 6144 the label index, 1536 entries of four -; 0x6800 16384 the label names, packed end to end -; 0xA800 256 one block of the output file, on its way to the disk -; 0xA900 17152 free +; 0x5000 8192 the label index, 2048 entries of four +; 0x7000 26624 the label names, packed end to end +; 0xD800 256 one block of the output file, on its way to the disk +; 0xD900 1792 free ; 0xE000 1792 the vector names, 64 entries of twenty eight ; 0xE700 2048 the reader's stack, six levels of 301 ; 0xEF00 368 which files have been included, sixteen names of 23 @@ -69,6 +69,17 @@ ; among buffers that do not need it, because an unclaimed page is available to whichever ; one turns out to want it. ; +; ---- And one turned out to want it ---- +; +; Sixteen kilobytes of names was half full when that was written and ran out on 2026-09-06, +; on an ordinary day's work on the shell: 16,758 bytes of name against 16,384. The index was +; 1,341 of 1,536 in the same breath, which is the paragraph above this one happening again - +; two ceilings, and lifting only the one that spoke would have bought two hundred labels. +; +; So both, out of the free page, and both left at about a third clear rather than at half: +; the names to 26,624 and the index to 2,048. What is left over is 1,792 bytes, kept +; unclaimed for the same reason the seventeen kilobytes were. +; ; That ends at 0xF070, with the Stack coming down from 0xFFFF above it - nearly four ; kilobytes, against the tens of bytes of CALL frames this ever nests. ; @@ -78,16 +89,17 @@ ; list moved up rather than the stack simply being asked to fit. ; ; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build -; and the largest one it will be asked to read. CosmOS is 475 labels and 9,564 bytes of -; output; the assembler itself is 555 labels, about 6,800 bytes of name and 11,648 of output. The -; second is bigger than the first, which is worth knowing: the hardest thing this assembles -; is not the operating system, it is itself. +; and the largest one it will be asked to read. Measured 2026-09-06: CosmOS is 1,341 labels +; and 16,758 bytes of name, and the assembler itself is far smaller. It was the other way +; round when this file was written - "the hardest thing this assembles is not the operating +; system, it is itself" - and the system has grown past it since. The numbers here are worth +; measuring again rather than believing, which is why they carry a date. ScratchLabIndex: 0x50 0x00 ScratchLabArena: - 0x68 0x00 + 0x70 0x00 ScratchWindow: - 0xA8 0x00 + 0xD8 0x00 ScratchVecNames: 0xE0 0x00 ScratchSrcStack: diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 282605d..3052b22 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -481,6 +481,23 @@ program; anything after it is a file. A directory answers with a separator on the end instead of a space, which says what it is and lets the next part be typed straight away. +**And when there is more than one, they are listed in columns**, sized to the longest of them, +the same way `ls` lays a directory out. The width comes from the screen and the longest name +is counted on the pass that finds the answer - which already visits every match, so the +listing costs no walk of its own to know how wide a column should be. + +**Directories are blue and the shell's own commands are green.** Both are known for nothing: +Tab appends the separator itself, and a built-in is not a file at all - which is also why no +amount of reading the disk could have told you it will run. + +**A file is not coloured, and that is a decision rather than an omission.** 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 first, which is a +buffer the shell would carry whether anybody pressed Tab or not - and `ls` is one keystroke +away. + **Files are looked for in the three places the shell would look to run one**: where you are, `/Apps` on the disk you are on, and `/Apps` on drive 0. Offering something the shell would not find would be finishing a word into a thing that then does not work. A file that is in two of diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index f5f84e7..86607a3 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -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 diff --git a/Tests/expected/cfgFallback.out b/Tests/expected/cfgFallback.out index 863f47d..4d3c5be 100644 --- a/Tests/expected/cfgFallback.out +++ b/Tests/expected/cfgFallback.out @@ -14,7 +14,7 @@ made > cd Notes /Notes> dir 0 files -8 of 32 entries, 309 blocks free +8 of 32 entries, 307 blocks free /Notes> exit halted Execution halted. diff --git a/Tests/expected/cosmosTab.out b/Tests/expected/cosmosTab.out index 4221db8..7511a6a 100644 --- a/Tests/expected/cosmosTab.out +++ b/Tests/expected/cosmosTab.out @@ -5,7 +5,7 @@ one > mkmkdir /made-by-tab made > d -dir do drive delete +dir do drive delete > d I do not know: d > zzzx diff --git a/Tests/expected/onceAsked.out b/Tests/expected/onceAsked.out index d62dd05..f1a7fe4 100644 --- a/Tests/expected/onceAsked.out +++ b/Tests/expected/onceAsked.out @@ -5,7 +5,7 @@ next start: /System/Boot/bare.bin, once System Apps 0 files, 2 directories -8 of 32 entries, 306 blocks free +8 of 32 entries, 304 blocks free > exit halted Execution halted. diff --git a/Tests/expected/romBoot.out b/Tests/expected/romBoot.out index 0d7cce3..53a20d9 100644 --- a/Tests/expected/romBoot.out +++ b/Tests/expected/romBoot.out @@ -12,7 +12,7 @@ made > cd Notes /Notes> dir 0 files -7 of 32 entries, 310 blocks free +7 of 32 entries, 308 blocks free /Notes> exit halted Execution halted. diff --git a/Tests/expected/selfBoot.out b/Tests/expected/selfBoot.out index 0d7cce3..53a20d9 100644 --- a/Tests/expected/selfBoot.out +++ b/Tests/expected/selfBoot.out @@ -12,7 +12,7 @@ made > cd Notes /Notes> dir 0 files -7 of 32 entries, 310 blocks free +7 of 32 entries, 308 blocks free /Notes> exit halted Execution halted.