Files
AnachronautandClaude Opus 5 e20c9bac1f The label table is kept in order, and halved instead of walked
labFind walked the index from the front, so every use of every label
cost a scan of every label defined so far, each with a string compare.
The cost grew with the program being built, which is what made it hurt:
assembling CosmOS on the machine took 1,833,691,267 cycles against the
assembler assembling itself at 57,257,133 - three times the source for
thirty two times the time.

Sorted and halved, the same build is 886,498,996. THE WALK WAS 52 PER
CENT OF THE WHOLE ASSEMBLY, which settles a suspicion this project has
carried unverified for weeks and puts a number on it.

Eleven comparisons against two thousand entries where a walk averaged six
hundred and seventy. The search hands back where a name WOULD go, which
is what adding one needs and what a walk could never have offered, so
labAdd gets its insertion point for nothing.

sameText was already an ordering and did not have to change: Q is the
difference at the first character that differed, and the Carry Flag from
that same subtraction survives the return because nothing puts the Status
register back. A name that runs out while the other carries on borrows
against the other's character, which sorts the shorter first.

Small programs pay about a tenth more - 57.3M to 63.5M for the assembler
on itself - because adding a label now moves the tail of the index up and
a short table was never expensive to walk. That is the right way round
for a trade to fall.

numHalve and numBack are new: a rotate right on a CIRCULAR sixteen bit
register brings bit nought back in at the top, so halving means taking
that bit off again, and the low half has to go down first because the
mask wants B.

WHICH END THE TABLE IS SORTED FROM DOES NOT MATTER. labAdd takes its
insertion point from labFind, so the comparison that decides the order is
the same one that searches it - turn it round and the table is built
backwards and read backwards and no output changes. Tests/break.sh says
so, correctly, by not noticing.

Verified by CosmOS builds CosmOS and second generation staying byte
identical. An indexing bug cannot hide behind a fixed point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 21:02:05 -04:00

180 lines
3.9 KiB
NASM

; The small things every other part of the assembler needs: sixteen bit arithmetic, for
; something that counts in addresses, and one string comparison.
;
; sbfs.asm has routines like these and the assembler cannot use them: it does not include
; the filesystem, because it reaches the disk through the system's services instead. That
; is the no-linker tax, paid in about a hundred and fifty bytes, and it is cheaper than the
; two and a half kilobytes including sbfs.asm would cost.
;
; Everything here works on numbers in memory rather than in registers, because a CALL puts
; A, B and Data Pointers 0 to 2 back as it found them. Only memory survives a return.
;
; Numbers are stored most significant byte first, the way every number on this machine is.
;
; Written by Anachronaut
#Program
; The two byte number at DP0 becomes the one at DP2. Destination first, so a call reads
; the way an assignment does.
numSet:
LDA.2
STA.0
INCD.2
INCD.0
LDA.2
STA.0
RET
; The two byte number at DP0 becomes itself plus the one at DP2.
numAdd:
INCD.0
INCD.2
LDA.0
LDB.2
CCF
ADD
MVQA
STA.0
DECD.0
DECD.2
LDA.0
LDB.2
ADD ; Carries in from the low half. Nothing between touches it.
MVQA
STA.0
RET
; The two byte number at DP0 becomes itself less the one at DP2.
numTake:
INCD.0
INCD.2
LDA.0
LDB.2
CCF
SUB
MVQA
STA.0
DECD.0
DECD.2
LDA.0
LDB.2
SUB ; Borrows in from the low half.
MVQA
STA.0
RET
; Adds the byte in A to the two byte number at DP0.
numAddByte:
INCD.0
LDB.0
CCF
ADD
MVQA
STA.0
BNC numAddByteDone
DECD.0
LDA.0
INCA
STA.0
numAddByteDone:
RET
; Adds one to the two byte number at DP0.
numStep:
INCD.0
LDA.0
INCA
STA.0
BNC numStepDone ; It did not wrap, so the high byte is untouched.
DECD.0
LDA.0
INCA
STA.0
numStepDone:
RET
; Compares the two byte number at DP0 with the one at DP2. Q is zero if they are equal,
; and the Carry Flag is set if the one at DP0 is the smaller. Both come back, because
; neither Q nor the Status register is put back by a return.
numCompare:
LDA.0
LDB.2
CCF
SUB ; The high bytes settle it unless they are the same.
BNQ numCompareDone
INCD.0
INCD.2
LDA.0
LDB.2
CCF
SUB
numCompareDone:
RET
; Takes one off the two byte number at DP0. The mirror of numStep, and wanted for the same
; reason: walking an index backwards is what moving a run of entries up needs.
numBack:
INCD.0
LDA.0
BNA numBackLow ; The low half has something to take, so the high half is safe.
DECD.0
LDA.0
DECA
STA.0
INCD.0
LDA.0
numBackLow:
DECA
STA.0
RET
; The two byte number at DP0 becomes half of itself.
;
; A and B are a CIRCULAR sixteen bit register, so a rotate right brings bit nought back in at
; the top rather than dropping it - which is a halving only once that bit is taken off again.
; The numbers this is asked about are index positions, well under thirty two thousand, so the
; top bit was nought before the rotate and clearing it afterwards loses nothing.
numHalve:
LDA.0
INCD.0
LDB.0
SHR
; The low half goes down FIRST, because taking the wrapped bit off the high half wants B
; for the mask and there is nowhere else to keep it.
STB.0
INIB 0x7F
AND
MVQA
DECD.0
STA.0
RET
; Q is zero if the strings at DP0 and DP1 are the same, both ending in a zero byte.
;
; Down here rather than with the label table, where it started, because four separate
; parts want it: labels, vector names, which file has already been included, and which
; directive a keyword is.
sameText:
LDA.0
LDB.1
CCF
SUB
BNQ sameTextDone
LDA.0
BRA sameTextDone ; They ended together, so they matched all the way.
INCD.0
INCD.1
BRI sameText
sameTextDone:
RET
; The two byte number at DP0 becomes zero.
numZero:
RSTA
STA.0
INCD.0
STA.0
RET