SplitBit assembles SplitBit: M1, a single file with no includes
Programs/CosmOS/Assembler/ is an assembler written in SplitBit assembly. It
runs under CosmOS, reads source off a SplitBit disk, and writes a binary back
to it with no host involved anywhere:
> run Asm.sbx hello.asm
wrote hello.bin: program 17, data 14, labels 2
THE ACCEPTANCE TEST IS THE BYTES. Tests/native.sh assembles Programs/hello.asm
both ways and compares the two files byte for byte, then runs the one the
machine built. "It ran" and "the sizes look right" both pass for a binary with
a label one byte out, which is a program that jumps into the middle of an
instruction - so the only honest test is the one SplitDisk and sbfs.asm
already work under: two implementations of one written specification, each
checking the other. The files are identical and the result prints Hello,
World! in 70 cycles.
hello.asm is the target because it is the oldest program in the repository.
The first thing this machine ever ran is now the first thing it assembles for
itself.
TWO PASSES OVER STREAMED SOURCE. The C assembler reads every token of every
file into one array; that cannot port, because cosmos.asm alone is 56,047
bytes against 64K of Data Memory. The native one streams through a 256 byte
window, twice, and keeps only the label table between the passes. Two passes
suffice because every length is known without resolving anything - an
instruction's from its shape, a value's is one, a string's is its characters
and a zero - so the first pass fixes every address and the second never needs
a fixup list. A forward reference stops being a special case and becomes the
reason there are two passes at all.
The parts, each checked before anything was built on it:
source.asm characters out of a file of any size, with a line number
token.asm tokens out of characters, one character of lookahead
classify.asm what a token is, in the C assembler's order, which IS the
language: keyword, instruction, value, string, label
labels.asm names packed in an arena, four bytes of index each
numbers.asm sixteen bit arithmetic, since sbfs.asm's cannot be reached
table.asm the instruction set, generated by the same script the
monitor's copy is, and now BOTH are checked by docs.sh
readTest.asm and tokenTest.asm check the reader and the tokenizer on their
own, recorded as cosmosSource and cosmosTokens. A wrong classification does
not produce a wrong byte somewhere obvious; it produces a right looking
program of the wrong length, so it is worth catching where it happens.
WHAT IT REFUSES: #Include, #Base, #Align, #Reserve and #Vectors are refused
by name rather than ignored. Skipping a directive would produce a file that
looked right and was the wrong length, which is the worst thing an assembler
can do.
Two traps worth recording, both already known to this project and both hit
again: CALL restores A, B and DP0-DP2, so three routines returning an answer
in A had it undone by their own return; and numStep works on DP0, so three
sites that set DP1 left a pointer that never advanced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
a131a90c67
commit
dcb331c151
@@ -0,0 +1,225 @@
|
||||
; The source reader: characters out of a file of any size.
|
||||
;
|
||||
; Everything else in the assembler sits on this, so it is the first thing built and the
|
||||
; thing most worth getting right. It hands out one character at a time and keeps a line
|
||||
; number, which is what lets an error say where it happened rather than only what it was.
|
||||
;
|
||||
; A FILE IS NEVER HELD WHOLE. It arrives a block at a time through osFileBlock, into one
|
||||
; buffer of 256 bytes, and is fetched again when the buffer runs out. That is why the
|
||||
; assembler can read a source file bigger than the memory it runs in - which cosmos.asm,
|
||||
; at 56,047 bytes, already is.
|
||||
;
|
||||
; The file is read TWICE, once per pass, and srcRewind is how the second pass starts over.
|
||||
; Nothing is kept between the passes but the label table.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Program
|
||||
|
||||
; Opens the file DP0 names. Q is zero if it is there.
|
||||
;
|
||||
; The name is copied rather than pointed at, because the caller's copy is in the caller's
|
||||
; memory and every later block read has to name the file again - there being no such thing
|
||||
; as an open file to hold on to.
|
||||
srcOpen:
|
||||
SETD.1 SrcName
|
||||
CALL srcKeepName
|
||||
CALL srcRewind
|
||||
RET
|
||||
|
||||
; Back to the first character, for the second pass.
|
||||
srcRewind:
|
||||
SETD.0 SrcIndex
|
||||
CALL numZero
|
||||
SETD.0 SrcAt
|
||||
CALL numZero
|
||||
SETD.0 SrcCount
|
||||
CALL numZero
|
||||
RSTA
|
||||
SETD.0 SrcEnded
|
||||
STA.0
|
||||
|
||||
; The line number counts from one, the way an editor does.
|
||||
SETD.0 SrcLine
|
||||
CALL numZero
|
||||
SETD.0 SrcLine
|
||||
CALL numStep
|
||||
|
||||
; Ask how big it is, which is both the answer to "is it there" and the thing that says
|
||||
; when to stop asking for blocks.
|
||||
SETD.0 SrcName
|
||||
SWI osFileInfo
|
||||
BNQ srcRewindNo
|
||||
PSHD.3
|
||||
POPB
|
||||
POPA
|
||||
SETD.0 SrcBlocks
|
||||
STA.0
|
||||
INCD.0
|
||||
STB.0
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD ; Q is zero: it is there.
|
||||
RET
|
||||
|
||||
srcRewindNo:
|
||||
INIA 0d1
|
||||
SETD.0 SrcEnded
|
||||
STA.0
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD ; Q is not zero: it is not.
|
||||
RET
|
||||
|
||||
; The next character of the file, into SrcChar. Q is zero if there was one, and something
|
||||
; else at the end of the file.
|
||||
srcNext:
|
||||
SETD.0 SrcEnded
|
||||
LDA.0
|
||||
BNA srcAtEnd
|
||||
|
||||
; Is the buffer used up? SrcAt counts how far into it we have read and SrcCount how many
|
||||
; of its bytes are the file's, which is 256 for every block but a short last one.
|
||||
SETD.0 SrcAt
|
||||
SETD.2 SrcCount
|
||||
CALL numCompare
|
||||
BNQ srcHaveByte
|
||||
CALL srcLoad
|
||||
BNQ srcAtEnd
|
||||
|
||||
srcHaveByte:
|
||||
SETD.1 SrcPointer
|
||||
LDD.0.1
|
||||
LDA.0
|
||||
INCD.0
|
||||
STD.0.1
|
||||
SETD.0 SrcChar
|
||||
STA.0
|
||||
SETD.0 SrcAt
|
||||
CALL numStep
|
||||
|
||||
; A newline is what makes the next character part of the next line. Counting it here,
|
||||
; as it is handed out, means the line number always describes the character just given.
|
||||
SETD.0 SrcChar
|
||||
LDA.0
|
||||
INIB 0x0A
|
||||
XOR
|
||||
BNQ srcNextDone
|
||||
SETD.0 SrcLine
|
||||
CALL numStep
|
||||
|
||||
srcNextDone:
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD ; Q is zero: there was a character.
|
||||
RET
|
||||
|
||||
srcAtEnd:
|
||||
INIA 0d1
|
||||
SETD.0 SrcEnded
|
||||
STA.0
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD ; Q is not zero: the file is finished.
|
||||
RET
|
||||
|
||||
; Fetches the block SrcIndex names, and steps SrcIndex past it. Q is zero if there was one.
|
||||
;
|
||||
; Running off the end is not a failure here: osFileBlock answers three for a block past the
|
||||
; end of the file, which is how a reader finds out it has finished. Any other refusal is a
|
||||
; real one, and both come back the same way because there is nothing useful to do about
|
||||
; either except stop.
|
||||
srcLoad:
|
||||
SETD.0 SrcName
|
||||
SETD.1 SrcBuffer
|
||||
SETD.2 SrcIndex
|
||||
LDA.2
|
||||
INCD.2
|
||||
LDB.2
|
||||
SWI osFileBlock
|
||||
BNQ srcLoadNo
|
||||
|
||||
; DP3 says how many of the block's bytes belong to the file: a whole 256 except in a
|
||||
; short last one, which is why it comes back in a pointer and not a register.
|
||||
PSHD.3
|
||||
POPB
|
||||
POPA
|
||||
SETD.0 SrcCount
|
||||
STA.0
|
||||
INCD.0
|
||||
STB.0
|
||||
|
||||
SETD.0 SrcAt
|
||||
CALL numZero
|
||||
SETD.0 SrcIndex
|
||||
CALL numStep
|
||||
|
||||
; The walking pointer starts at the front of the buffer again.
|
||||
SETD.0 SrcBuffer
|
||||
SETD.1 SrcPointer
|
||||
STD.0.1
|
||||
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
srcLoadNo:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; Copies the name at DP0 into DP1, up to 22 characters of it and the zero after them,
|
||||
; which is as long as a name on this filesystem may be.
|
||||
srcKeepName:
|
||||
INIA 0d22
|
||||
SETD.2 SrcLeft
|
||||
STA.2
|
||||
srcKeepLoop:
|
||||
LDA.0
|
||||
BRA srcKeepEnd
|
||||
STA.1
|
||||
INCD.0
|
||||
INCD.1
|
||||
LDA.2
|
||||
DECA
|
||||
STA.2
|
||||
BNA srcKeepLoop
|
||||
srcKeepEnd:
|
||||
RSTA
|
||||
STA.1 ; The zero that makes it a string.
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
SrcName:
|
||||
#Reserve 0d23
|
||||
SrcBlocks:
|
||||
0x00 0x00
|
||||
SrcIndex:
|
||||
0x00 0x00
|
||||
SrcCount:
|
||||
0x00 0x00
|
||||
SrcAt:
|
||||
0x00 0x00
|
||||
SrcLine:
|
||||
0x00 0x00
|
||||
SrcPointer:
|
||||
0x00 0x00
|
||||
SrcEnded:
|
||||
0x00
|
||||
SrcChar:
|
||||
0x00
|
||||
SrcLeft:
|
||||
0x00
|
||||
|
||||
; One block, which is the whole of what a source file costs in memory however big it is.
|
||||
SrcBuffer:
|
||||
#Reserve 0d256
|
||||
Reference in New Issue
Block a user