Files
SplitBit-Emulator/Programs/CosmOS/Assembler/labels.asm
T
AnachronautandClaude Opus 5 563bc20a75 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
2026-09-06 20:18:56 -04:00

330 lines
6.8 KiB
NASM

; The label table: the only thing that survives between the two passes.
;
; Names are packed end to end in an arena and each index entry holds a pointer into it,
; rather than every entry carrying a field wide enough for the longest name. MEASURED on
; CosmOS, which is the biggest thing this will ever be asked to assemble: 453 labels
; averaging 11.3 characters. Packed they come to about 7,400 bytes; in 32 byte fields they
; would come to 15,400. The arena is worth the handful of extra instructions.
;
; Four bytes an index entry: two saying where the name is, two saying what it resolves to.
;
; A name is stored WITHOUT its colon, so that a definition and a use of it compare equal
; without either side having to know which it was looking at.
;
; The first pass fills this and the second only reads it. That is what makes a forward
; reference ordinary rather than special: by the time anything is emitted, every name in
; the program already has an address.
;
; Written by Anachronaut
#Program
; Empties the table.
labReset:
SETD.0 LabCount
CALL numZero
SETD.0 LabUsed
CALL numZero
SETD.0 LabNext
SETD.2 ScratchLabArena
CALL numSet
SETD.0 LabBase
SETD.2 ScratchLabIndex
CALL numSet
RET
; Adds the name at DP0, meaning the address in A and B. Q is zero if it went in.
;
; A name already in the table is refused rather than replaced: one name may mean one place,
; and quietly taking the second would move everything that referred to the first.
labAdd:
SETD.2 LabPutAddress
STA.2
INCD.2
STB.2
SETD.2 LabSubject
STD.0.2
CALL labFind
BNQ labAddFresh
SETD.0 LabTwice
CALL labComplain
BRI labAddNo
labAddFresh:
SETD.0 LabCount
SETD.2 LabLimit
CALL numCompare
BNC labAddFull ; The index is as full as it goes.
; And the arena, counting the zero that ends the name.
SETD.1 LabSubject
LDD.0.1
CALL labLength
SETD.0 LabEnd
SETD.2 LabUsed
CALL numSet
SETD.0 LabEnd
SETD.2 LabLength
CALL numAdd
SETD.0 LabRoom
SETD.2 LabEnd
CALL numCompare
BRC labAddCrowded ; The arena is smaller than where this name would end.
; The index entry: where the name is about to go, and what it means.
SETD.0 LabWhich
SETD.2 LabCount
CALL numSet
CALL labEntryAt
SETD.1 LabEntry
LDD.0.1
SETD.1 LabNext
LDD.1.1
PSHD.1
POPB
POPA ; The low byte is on top, the way a pointer is pushed.
STA.0
INCD.0
STB.0
INCD.0
SETD.2 LabPutAddress
LDA.2
STA.0
INCD.0
INCD.2
LDA.2
STA.0
; And the name itself, into the arena.
SETD.1 LabNext
LDD.1.1
SETD.2 LabSubject
LDD.0.2
labAddLoop:
LDA.0
STA.1
BRA labAddCopied
INCD.0
INCD.1
BRI labAddLoop
labAddCopied:
INCD.1 ; Past the zero, which was copied with the rest.
SETD.0 LabNext
STD.1.0
SETD.0 LabUsed
SETD.2 LabLength
CALL numAdd
SETD.0 LabCount
CALL numStep
RSTA
RSTB
CCF
ADD
RET
labAddFull:
SETD.0 LabFull
CALL labComplain
BRI labAddNo
labAddCrowded:
SETD.0 LabNoRoom
CALL labComplain
labAddNo:
RSTA
INIB 0d1
CCF
ADD
RET
; Looks up the name at DP0. Q is zero if it is there, and then LabAddress is what it means.
;
; A straight walk from the front. With 453 labels and a few thousand uses of them that is
; the slowest thing the assembler does, and it is deliberately the simple version: sorting
; the table or bucketing it on the first character are both easy later, and neither is
; worth writing before anything has been measured.
labFind:
SETD.2 LabSought
STD.0.2
SETD.0 LabWhich
CALL numZero
labFindLoop:
SETD.0 LabWhich
SETD.2 LabCount
CALL numCompare
BNC labFindMissing ; Walked the whole table without a match.
CALL labEntryAt
SETD.1 LabEntry
LDD.0.1
LDA.0
INCD.0
LDB.0
SETD.0 LabNamePointer
STA.0
INCD.0
STB.0
SETD.1 LabNamePointer
LDD.0.1
SETD.1 LabSought
LDD.1.1
CALL sameText
BRQ labFindGot
SETD.0 LabWhich
CALL numStep
BRI labFindLoop
labFindGot:
SETD.1 LabEntry
LDD.0.1
INCD.0
INCD.0
LDA.0
INCD.0
LDB.0
SETD.0 LabAddress
STA.0
INCD.0
STB.0
RSTA
RSTB
CCF
ADD
RET
labFindMissing:
RSTA
INIB 0d1
CCF
ADD
RET
; Where entry number LabWhich is, into LabEntry. Four bytes an entry, so the offset is the
; number doubled twice - there being no multiply on this machine, and none needed.
labEntryAt:
SETD.0 LabOffset
SETD.2 LabWhich
CALL numSet
SETD.0 LabOffset
SETD.2 LabOffset
CALL numAdd
SETD.0 LabOffset
SETD.2 LabOffset
CALL numAdd
SETD.0 LabEntry
SETD.2 LabBase
CALL numSet
SETD.0 LabEntry
SETD.2 LabOffset
CALL numAdd
RET
; How long the string at DP0 is, counting the zero on the end, into LabLength.
labLength:
SETD.1 LabLenWalk
STD.0.1
SETD.0 LabLength
CALL numZero
labLengthLoop:
SETD.0 LabLength
CALL numStep
SETD.1 LabLenWalk
LDD.0.1
LDA.0
BRA labLengthDone
SETD.0 LabLenWalk
CALL numStep
BRI labLengthLoop
labLengthDone:
RET
labComplain:
SWI osPrintString
SETD.0 LabNamed
SWI osPrintString
SETD.0 TokText
SWI osPrintString
SETD.0 LabAtLine
SWI osPrintString
SETD.0 TokLine
LDA.0
INCD.0
LDB.0
SWI osPrintNumber
SETD.0 LabNewLine
SWI osPrintString
RET
#Data
LabCount:
0x00 0x00
LabUsed:
0x00 0x00
LabNext:
0x00 0x00
LabBase:
0x00 0x00
LabAddress:
0x00 0x00
LabPutAddress:
0x00 0x00
LabNamePointer:
0x00 0x00
LabSought:
0x00 0x00
LabSubject:
0x00 0x00
LabEntry:
0x00 0x00
LabOffset:
0x00 0x00
LabWhich:
0x00 0x00
LabLength:
0x00 0x00
LabLenWalk:
0x00 0x00
LabEnd:
0x00 0x00
; 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: 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.
; 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:
0x08 0x00
LabRoom:
0x68 0x00
LabNamed:
": "
LabAtLine:
", at line "
LabNewLine:
"
"
LabTwice:
"that label is defined twice"
LabFull:
"too many labels"
LabNoRoom:
"no room left for label names"