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:
Anachronaut
2026-08-20 22:13:15 -04:00
co-authored by Claude Opus 5
parent a131a90c67
commit dcb331c151
21 changed files with 3185 additions and 7 deletions
+879
View File
@@ -0,0 +1,879 @@
; The SplitBit assembler, running on SplitBit.
;
; run Asm.sbx hello.asm
;
; Reads assembly source off the disk and writes a binary back to it, with no host involved
; anywhere. The output has to be byte for byte what the C assembler produces from the same
; source, which is the only honest test of it and the one the suite runs.
;
; ---- Two passes over a file that is never held ----
;
; The C assembler reads every token of every file into one array and works on that. It
; cannot be done that way here and never could: cosmos.asm alone is 56,047 bytes of source
; against 64K of Data Memory, and the token array for it would be several times that. So
; the source is streamed through a 256 byte window, twice, and the only thing kept between
; the passes is the label table.
;
; TWO PASSES ARE ENOUGH BECAUSE EVERY LENGTH IS KNOWN WITHOUT RESOLVING ANYTHING. How many
; bytes a token comes to falls out of what the token is - an instruction's from its shape,
; a value's is one, a string's is its characters and a zero - and never from the value of
; anything named. So the first pass can work out exactly where every label lands, and the
; second never needs a fixup list or a second look. A forward reference stops being a
; problem and becomes the reason there are two passes at all.
;
; ---- What this one does not do yet ----
;
; #Include, #Base, #Align, #Reserve and #Vectors are refused by name rather than ignored.
; An assembler that quietly skipped a directive would produce a file that looked right and
; was the wrong length, which is the worst thing it could do.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x2000
start:
SETD.0 Argument
INIB 0d23
SWI osArgument
SETD.0 Argument
LDA.0
BRA sayUsage
CALL deriveName
SETD.0 Argument
CALL srcOpen
BNQ noSource
CALL passOne
BNQ stopped
CALL layOutImage
BNQ stopped
CALL passTwo
BNQ stopped
CALL writeImage
BNQ stopped
CALL report
SWI osExit
stopped:
SETD.0 StoppedText
SWI osPrintString
SWI osExit
sayUsage:
SETD.0 UsageText
SWI osPrintString
SWI osExit
noSource:
SETD.0 NoSourceText
SWI osPrintString
SETD.0 Argument
SWI osPrintString
SETD.0 NewLine
SWI osPrintString
SWI osExit
; ---- The first pass: how long everything is, and where every label lands ----
passOne:
CALL labReset
CALL beginPass
oneLoop:
CALL tokNext
BNQ oneDone
CALL clsToken
BNQ passFailed
SETD.0 ClsType
LDA.0
INIB 0d0
XOR
BRQ oneKeyword
SETD.0 ClsType
LDA.0
INIB 0d4
XOR
BRQ oneDefinition
CALL checkPlacement
BNQ passFailed
BRI oneAdvance
oneKeyword:
CALL doKeyword
BNQ passFailed
BRI oneLoop ; A keyword is no bytes, so there is nothing to advance.
oneDefinition:
; The colon is not part of the name. Writing a zero over it here means a definition and
; a use of the same name compare equal without either side knowing which it is.
CALL dropColon
SETD.0 Status
LDA.0
BRA labelNowhere
SETD.0 ProgAt
SETD.2 Status
LDA.2
INIB 0d1
XOR
BRQ oneDefineHere
SETD.0 DataAt
oneDefineHere:
LDA.0
INCD.0
LDB.0
SETD.0 TokText
CALL labAdd
BNQ passFailed
BRI oneLoop ; A definition is a name for a place, not a byte in it.
oneAdvance:
CALL stepCursor
BRI oneLoop
oneDone:
RSTA
RSTB
CCF
ADD
RET
labelNowhere:
SETD.0 LabelNowhereText
CALL clsComplain
passFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- The second pass: the bytes themselves ----
passTwo:
CALL srcRewind
BNQ passFailed
CALL beginStatus ; The cursors are NOT reset. They are the first pass's answer,
; the second pass writes through pointers of its own, and the
; report at the end still has to say how long the segments are.
twoLoop:
CALL tokNext
BNQ twoDone
CALL clsToken
BNQ passFailed
SETD.0 ClsType
LDA.0
SETD.0 EmitKind
STA.0
INIB 0d0
XOR
BRQ twoKeyword
SETD.0 EmitKind
LDA.0
INIB 0d1
XOR
BRQ twoInstruction
SETD.0 EmitKind
LDA.0
INIB 0d2
XOR
BRQ twoValue
SETD.0 EmitKind
LDA.0
INIB 0d3
XOR
BRQ twoString
SETD.0 EmitKind
LDA.0
INIB 0d4
XOR
BRQ twoLoop ; A definition emits nothing; the first pass took its address.
BRI twoReference
twoKeyword:
CALL doKeyword
BNQ passFailed
BRI twoLoop
twoInstruction:
SETD.0 ClsOpcode
LDA.0
CALL emitByte
; The selectors follow the opcode, and they are written whether or not the programmer
; wrote them: leaving one off means Data Pointer 0 rather than no pointer at all.
RSTA
SETD.0 EmitLeft
STA.0
twoSelectorLoop:
SETD.0 EmitLeft
LDA.0
SETD.2 ClsWanted
LDB.2
CCF
SUB
BRQ twoLoop
SETD.0 ClsSelectorValue
SETD.2 EmitLeft
LDA.2
CALL byteAt
SETD.0 ClsByte
LDA.0
CALL emitByte
SETD.0 EmitLeft
LDA.0
INCA
STA.0
BRI twoSelectorLoop
twoValue:
SETD.0 ClsValue
LDA.0
CALL emitByte
BRI twoLoop
twoString:
SETD.0 TokText
SETD.1 EmitWalk
STD.0.1
twoStringLoop:
SETD.1 EmitWalk
LDD.0.1
LDA.0
CALL emitByte
SETD.0 EmitWalk
CALL numStep
SETD.1 EmitWalk
LDD.0.1
DPDN.0 0d01
LDA.0
BNA twoStringLoop ; The zero goes out with the rest and then stops the loop.
BRI twoLoop
twoReference:
SETD.0 TokText
CALL labFind
BNQ twoUnknown
SETD.0 LabAddress
LDA.0
CALL emitByte
SETD.0 LabAddress
INCD.0
LDA.0
CALL emitByte
BRI twoLoop
twoUnknown:
SETD.0 UnknownText
CALL clsComplain
BRI passFailed
twoDone:
RSTA
RSTB
CCF
ADD
RET
; ---- What both passes have in common ----
beginPass:
CALL beginStatus
SETD.0 ProgAt
CALL numZero
SETD.0 DataAt
CALL numZero
RET
; No segment is open until a #Program or #Data says so, at the start of either pass.
beginStatus:
RSTA
SETD.0 Status
STA.0
RET
; Moves the cursor of whichever segment is open along by what this token comes to.
stepCursor:
SETD.0 Status
LDA.0
INIB 0d1
XOR
BRQ stepProgram
SETD.0 DataAt
BRI stepBy
stepProgram:
SETD.0 ProgAt
stepBy:
SETD.2 ClsLength
LDA.2
CALL numAddByte
RET
; Is this token allowed where it is? The rules are the C assembler's, and they exist
; because each of these has a way of going wrong quietly.
checkPlacement:
SETD.0 ClsType
LDA.0
INIB 0d1
XOR
BRQ placeInstruction
SETD.0 ClsType
LDA.0
INIB 0d3
XOR
BRQ placeString
; A value or a name, which needs somewhere to go but does not care which.
SETD.0 Status
LDA.0
BRA placeNowhere
BRI placeYes
placeInstruction:
SETD.0 Status
LDA.0
INIB 0d1
XOR
BNQ placeNotProgram
BRI placeYes
placeString:
; A string in Program Memory could not be read by the program holding it: instructions
; reach Data Memory only. It would assemble and then be unreachable.
SETD.0 Status
LDA.0
INIB 0d1
XOR
BRQ placeStringInProgram
SETD.0 Status
LDA.0
BRA placeNowhere
placeYes:
RSTA
RSTB
CCF
ADD
RET
placeNowhere:
SETD.0 NowhereText
CALL clsComplain
BRI placeNo
placeNotProgram:
SETD.0 NotProgramText
CALL clsComplain
BRI placeNo
placeStringInProgram:
SETD.0 StringInProgramText
CALL clsComplain
placeNo:
RSTA
INIB 0d1
CCF
ADD
RET
; #Program and #Data change which segment is open. Everything else the C assembler
; understands is refused by name, because an assembler that skipped a directive would
; produce a file that looked right and was the wrong length.
doKeyword:
SETD.0 TokText
SETD.1 WordProgram
CALL labSame
BRQ keywordProgram
SETD.0 TokText
SETD.1 WordData
CALL labSame
BRQ keywordData
SETD.0 NotYetText
CALL clsComplain
RSTA
INIB 0d1
CCF
ADD
RET
keywordProgram:
INIA 0d1
BRI keywordSet
keywordData:
INIA 0d2
keywordSet:
SETD.0 Status
STA.0
RSTA
RSTB
CCF
ADD
RET
; Writes a zero over the colon on the end of a label definition.
dropColon:
SETD.0 TokLength
LDA.0
BRA dropColonDone
DECA
SETD.0 TokLength
STA.0
SETD.0 TokText
SETD.1 DropWalk
STD.0.1
SETD.0 DropWalk
SETD.2 TokLength
LDA.2
CALL numAddByte
SETD.1 DropWalk
LDD.0.1
RSTA
STA.0
dropColonDone:
RET
; ---- The output image ----
; Where each segment's bytes will go, and the header in front of them. Both lengths are
; known now, which is the whole reason the first pass exists.
layOutImage:
; Nineteen bytes of format: the magic, a version, four feature flags, and a marker and
; a length for each of the two segments.
SETD.0 ImgTotal
SETD.2 ProgAt
CALL numSet
SETD.0 ImgTotal
SETD.2 DataAt
CALL numAdd
INIA 0d19
SETD.0 ImgTotal
CALL numAddByte
SETD.0 ImgRoom
SETD.2 ImgTotal
CALL numCompare
BRC imageTooBig
SETD.0 Image
SETD.1 ProgPut
STD.0.1
SETD.0 ProgPut
INIA 0d14
CALL numAddByte
SETD.0 ImgWalk
SETD.2 ProgAt
CALL numSet
INIA 0d19
SETD.0 ImgWalk
CALL numAddByte
SETD.0 Image
SETD.1 DataPut
STD.0.1
SETD.0 DataPut
SETD.2 ImgWalk
CALL numAdd
; The header, written straight into the front of the image.
SETD.0 Image
SETD.1 ImgWalk
STD.0.1
SETD.0 MagicSPBT
INIA 0d4
CALL putBytes
INIA 0d1
CALL putByte ; The format version.
RSTA
CALL putByte
CALL putByte
CALL putByte
CALL putByte ; Four bytes of feature flags, none of them asked for.
SETD.0 MagicPRG
INIA 0d3
CALL putBytes
SETD.0 ProgAt
CALL putWord
; And the marker between the segments, which sits after the program bytes.
SETD.0 Image
SETD.1 ImgWalk
STD.0.1
SETD.0 ImgWalk
INIA 0d14
CALL numAddByte
SETD.0 ImgWalk
SETD.2 ProgAt
CALL numAdd
SETD.0 MagicDAT
INIA 0d3
CALL putBytes
SETD.0 DataAt
CALL putWord
RSTA
RSTB
CCF
ADD
RET
imageTooBig:
SETD.0 TooBigText
SWI osPrintString
RSTA
INIB 0d1
CCF
ADD
RET
; Puts A down at ImgWalk and steps it.
putByte:
SETD.0 ImgHold
STA.0
SETD.1 ImgWalk
LDD.0.1
SETD.2 ImgHold
LDA.2
STA.0
INCD.0
STD.0.1
RET
; Puts A bytes from DP0 down at ImgWalk.
putBytes:
SETD.1 ImgCount
STA.1
SETD.1 ImgFrom
STD.0.1
putBytesLoop:
SETD.0 ImgCount
LDA.0
BRA putBytesDone
DECA
STA.0
SETD.1 ImgFrom
LDD.0.1
LDA.0
CALL putByte
SETD.0 ImgFrom
CALL numStep
BRI putBytesLoop
putBytesDone:
RET
; Puts the two byte number at DP0 down at ImgWalk, most significant first, the way every
; number in this format is stored.
putWord:
SETD.1 ImgFrom
STD.0.1
LDA.0
CALL putByte
SETD.1 ImgFrom
LDD.0.1
INCD.0
LDA.0
CALL putByte
RET
; Puts A into whichever segment is open, and steps that segment's pointer.
emitByte:
SETD.0 EmitHold
STA.0
SETD.0 Status
LDA.0
INIB 0d1
XOR
BRQ emitToProgram
SETD.1 DataPut
BRI emitPut
emitToProgram:
SETD.1 ProgPut
emitPut:
LDD.0.1
SETD.2 EmitHold
LDA.2
STA.0
INCD.0
STD.0.1
RET
; The byte at DP0 offset by A, into ClsByte. The classifier has one of these; this is the
; assembler's, because a routine over there answers into a variable over there.
byteAt:
PSHA
PSHD.0
POPB
POPA
SETD.0 EmitWalk
STA.0
INCD.0
STB.0
POPA
SETD.0 EmitWalk
CALL numAddByte
SETD.1 EmitWalk
LDD.0.1
LDA.0
SETD.0 ClsByte
STA.0
RET
writeImage:
SETD.0 OutName
SETD.1 Image
SETD.2 ImgTotal
LDA.2
INCD.2
LDB.2
SWI osFileSave
BNQ writeFailed
RSTA
RSTB
CCF
ADD
RET
writeFailed:
SETD.0 NoWriteText
SWI osPrintString
SETD.0 OutName
SWI osPrintString
SETD.0 NewLine
SWI osPrintString
RSTA
INIB 0d1
CCF
ADD
RET
; What the source file is called with its extension replaced, so that hello.asm becomes
; hello.bin without anybody having to say so twice.
deriveName:
SETD.0 Argument
SETD.1 OutName
CALL copyName
SETD.0 OutName
SETD.1 DotAt
STD.0.1
SETD.0 DotFound
CALL numZero
SETD.0 OutName
SETD.1 NameWalk
STD.0.1
deriveLoop:
SETD.1 NameWalk
LDD.0.1
LDA.0
BRA deriveEnd
INIB 0x2E ; '.'
XOR
BNQ deriveStep
SETD.0 DotAt
SETD.2 NameWalk
CALL numSet
INIA 0d1
SETD.0 DotFound
STA.0
deriveStep:
SETD.0 NameWalk
CALL numStep
BRI deriveLoop
deriveEnd:
SETD.0 DotFound
LDA.0
BNA deriveAtDot
SETD.0 DotAt
SETD.2 NameWalk
CALL numSet ; No extension at all, so the new one goes on the end.
deriveAtDot:
SETD.1 DotAt
LDD.1.1
SETD.0 Extension
deriveCopy:
LDA.0
STA.1
BRA deriveDone
INCD.0
INCD.1
BRI deriveCopy
deriveDone:
RET
; Copies the string at DP0 to DP1, up to 22 characters and the zero after them.
copyName:
INIA 0d22
SETD.2 NameLeft
STA.2
copyNameLoop:
LDA.0
BRA copyNameEnd
STA.1
INCD.0
INCD.1
SETD.2 NameLeft
LDA.2
DECA
STA.2
BNA copyNameLoop
copyNameEnd:
RSTA
STA.1
RET
report:
SETD.0 WroteText
SWI osPrintString
SETD.0 OutName
SWI osPrintString
SETD.0 ProgramText
SWI osPrintString
SETD.0 ProgAt
LDA.0
INCD.0
LDB.0
SWI osPrintNumber
SETD.0 DataText
SWI osPrintString
SETD.0 DataAt
LDA.0
INCD.0
LDB.0
SWI osPrintNumber
SETD.0 LabelsText
SWI osPrintString
SETD.0 LabCount
LDA.0
INCD.0
LDB.0
SWI osPrintNumber
SETD.0 LabelsEnd
SWI osPrintString
RET
#Data
#Base 0x1000
Argument:
#Reserve 0d23
OutName:
#Reserve 0d27
NameWalk:
0x00 0x00
NameLeft:
0x00
DotAt:
0x00 0x00
DotFound:
0x00 0x00
Status:
0x00
ProgAt:
0x00 0x00
DataAt:
0x00 0x00
ProgPut:
0x00 0x00
DataPut:
0x00 0x00
EmitHold:
0x00
EmitKind:
0x00
EmitLeft:
0x00
EmitWalk:
0x00 0x00
ImgTotal:
0x00 0x00
ImgWalk:
0x00 0x00
ImgFrom:
0x00 0x00
ImgCount:
0x00
ImgHold:
0x00
DropWalk:
0x00 0x00
; How big a binary this can build. Everything the assembler makes has to fit here at once,
; because a file is written in one call and there is nowhere to put half of one.
ImgRoom:
0x10 0x00
MagicSPBT:
"SPBT"
MagicPRG:
"PRG"
MagicDAT:
"DAT"
Extension:
".bin"
WordProgram:
"#Program"
WordData:
"#Data"
UsageText:
"say which file: run Asm.sbx hello.asm
"
NoSourceText:
"no such file: "
NewLine:
"
"
NowhereText:
"that has to be inside a segment, and no #Program or #Data has opened one"
NotProgramText:
"an instruction outside the Program Segment"
StringInProgramText:
"a string cannot go in the Program Segment, because an instruction cannot read it there"
LabelNowhereText:
"a label defined outside a segment, so there is nowhere for it to point"
UnknownText:
"no label of that name is defined anywhere in this program"
NotYetText:
"this assembler does not understand that directive yet"
TooBigText:
"the binary would be bigger than this assembler has room to build
"
NoWriteText:
"it would not write "
StoppedText:
"nothing was written
"
WroteText:
"wrote "
ProgramText:
": program "
DataText:
", data "
LabelsText:
", labels "
LabelsEnd:
"
"
Image:
#Reserve 0d4096
#Include numbers.asm
#Include source.asm
#Include token.asm
#Include classify.asm
#Include labels.asm
#Include table.asm
+792
View File
@@ -0,0 +1,792 @@
; What a token is.
;
; THE ORDER OF THESE TESTS IS THE LANGUAGE, and it is copied deliberately from the C
; assembler rather than reinvented, because the two have to produce the same bytes from
; the same source. A token is a keyword, then an instruction, then a literal value, then a
; string, then a label - and what a thing means depends on which of those it reaches first.
;
; ClsType 0 keyword 1 instruction 2 value 3 string
; 4 label definition 5 label reference
;
; A STRING IS NEVER ANYTHING ELSE. The quotes are gone by the time a token is looked at, so
; without that guard a string whose text reads "ADD" assembles as an instruction and one
; that begins with a zero is rejected as a malformed literal. Both have happened; the C
; assembler carries the same guard in two places and this carries it in four, because the
; keyword test needs it too and over there it does not have it.
;
; Written by Anachronaut
#Program
; Works out what TokText is. Q is zero if it is something the assembler understands.
clsToken:
RSTA
SETD.0 ClsLength
STA.0
SETD.0 TokString
LDA.0
BNA clsIsString
; ---- A keyword ----
SETD.0 TokText
LDA.0
INIB 0x23 ; '#'
XOR
BNQ clsTryInstruction
INIA 0d0
SETD.0 ClsType
STA.0
BRI clsYes
clsTryInstruction:
CALL clsInstruction
BNQ clsTryValue
INIA 0d1
SETD.0 ClsType
STA.0
BRI clsYes
clsTryValue:
; A leading zero means a literal was meant, so anything malformed after it is an error
; rather than a label. Falling through to the label test would quietly emit two bytes
; where one was wanted and shift everything after it.
SETD.0 TokText
LDA.0
INIB 0x30 ; '0'
XOR
BNQ clsTryLabel
CALL clsValue
BNQ clsNo
INIA 0d2
SETD.0 ClsType
STA.0
INIA 0d1
SETD.0 ClsLength
STA.0
BRI clsYes
clsIsString:
INIA 0d3
SETD.0 ClsType
STA.0
; A string is its characters and the zero byte after them, which is why two strings
; written in a row are two strings rather than one long one.
SETD.0 TokLength
LDA.0
INCA
SETD.0 ClsLength
STA.0
BRI clsYes
clsTryLabel:
; A colon on the end makes it a definition. Everything else is a use of a name, which
; is two bytes of address wherever it appears.
CALL clsLastCharacter
SETD.0 ClsByte
LDA.0
INIB 0x3A ; ':'
XOR
BNQ clsUse
INIA 0d4
SETD.0 ClsType
STA.0
BRI clsYes
clsUse:
INIA 0d5
SETD.0 ClsType
STA.0
INIA 0d2
SETD.0 ClsLength
STA.0
clsYes:
RSTA
RSTB
CCF
ADD
RET
clsNo:
RSTA
INIB 0d1
CCF
ADD
RET
; The last character of the token, into ClsByte. Zero if the token is empty.
;
; INTO MEMORY, not into A, and that is not a style choice: a CALL saves and restores A, B
; and Data Pointers 0 to 2, so a routine that leaves its answer in one of those has the
; answer undone by its own return. Only Q, DP3 and memory survive.
clsLastCharacter:
SETD.0 TokLength
LDA.0
BRA clsLastNone
SETD.0 TokText
SETD.1 ClsWalk
STD.0.1
SETD.0 TokLength
LDA.0
DECA
SETD.0 ClsWalk
CALL numAddByte
SETD.1 ClsWalk
LDD.0.1
LDA.0
SETD.0 ClsByte
STA.0
RET
clsLastNone:
RSTA
SETD.0 ClsByte
STA.0
RET
; ---- Instructions ----
; Is TokText an instruction? Q is zero if it is, and then ClsOpcode, ClsShape, ClsLength
; and ClsSelectorValue describe it.
;
; The name is folded to upper case and the selectors are split off before anything is
; looked up, because SETD.2 is the instruction SETD naming Data Pointer 2 rather than a
; name of its own.
clsInstruction:
CALL clsSplitName
BNQ clsInstructionNo ; Longer than any mnemonic, so it is not one.
CALL clsFindName
BNQ clsInstructionNo
; How many selectors this shape wants. They are emitted whether or not they were
; written, so the length is fixed by the instruction and leaving one off means zero.
SETD.0 ClsShape
LDA.0
SETD.0 AsmShapeSelectors
CALL clsIndexByte
SETD.0 ClsByte
LDA.0
SETD.0 ClsWanted
STA.0
; More selectors than the instruction has pointers to name is a mistake worth catching:
; it means the programmer thinks it does something it does not.
SETD.0 ClsGiven
LDA.0
SETD.2 ClsWanted
LDB.2
CCF
SUB
BRQ clsSelectorsFit
BRC clsSelectorsFit ; Fewer than wanted is allowed and means zero.
SETD.0 TooManySelectors
CALL clsComplain
BRI clsInstructionNo
clsSelectorsFit:
SETD.0 ClsWanted
LDA.0
INCA
SETD.0 ClsLength
STA.0 ; The opcode and its selectors. The operand is its own token.
RSTA
RSTB
CCF
ADD
RET
clsInstructionNo:
RSTA
INIB 0d1
CCF
ADD
RET
; Splits TokText into an upper case mnemonic in ClsName, padded to four with spaces, and
; up to two selector digits in ClsSelectorValue. Q is zero if the name could be a mnemonic
; at all, which means four characters or fewer.
clsSplitName:
INIA 0x20
SETD.0 ClsName
STA.0
INCD.0
STA.0
INCD.0
STA.0
INCD.0
STA.0
INCD.0
RSTA
STA.0 ; Four spaces and a zero, so a short name still compares.
RSTA
SETD.0 ClsGiven
STA.0
SETD.0 ClsSelectorValue
STA.0
INCD.0
STA.0
SETD.0 ClsNameLength
RSTA
STA.0
SETD.0 TokText
SETD.1 ClsWalk
STD.0.1
clsNameLoop:
SETD.1 ClsWalk
LDD.0.1
LDA.0
BRA clsSplitDone
INIB 0x2E ; '.'
XOR
BRQ clsSelectorPart
SETD.0 ClsNameLength
LDA.0
INIB 0d4
CCF
SUB
BNC clsSplitTooLong ; A fifth character, so this is not a mnemonic.
SETD.1 ClsWalk
LDD.0.1
LDA.0
CALL clsUpper
SETD.0 ClsByte
LDA.0
SETD.0 ClsName
SETD.2 ClsNameLength
CALL clsPutIndexed
SETD.0 ClsNameLength
LDA.0
INCA
STA.0
clsNameStep:
SETD.0 ClsWalk
CALL numStep
BRI clsNameLoop
clsSelectorPart:
; The character after the dot is which Data Pointer, in decimal.
SETD.0 ClsWalk
CALL numStep
SETD.1 ClsWalk
LDD.0.1
LDA.0
BRA clsSplitDone
INIB 0x30
CCF
SUB
MVQA ; The digit as a number.
SETD.0 ClsDigitHold
STA.0
; There are four Data Pointers, so anything above three does not name one.
INIB 0d4
CCF
SUB
BNC clsSelectorRange
SETD.0 ClsGiven
LDA.0
INIB 0d2
CCF
SUB
BNC clsSelectorSpare ; Already two, so anything more is counted and discarded;
; the count is what the caller complains about.
SETD.0 ClsDigitHold
LDA.0
SETD.0 ClsSelectorValue
SETD.2 ClsGiven
CALL clsPutIndexed
clsSelectorSpare:
SETD.0 ClsGiven
LDA.0
INCA
STA.0
BRI clsNameStep
clsSplitDone:
SETD.0 ClsNameLength
LDA.0
BRA clsSplitTooLong ; Nothing before the dot is not a mnemonic either.
RSTA
RSTB
CCF
ADD
RET
clsSelectorRange:
SETD.0 BadSelector
CALL clsComplain
clsSplitTooLong:
RSTA
INIB 0d1
CCF
ADD
RET
; Looks ClsName up in the instruction table. Q is zero if it is there, and then ClsOpcode
; and ClsShape say what it is.
clsFindName:
SETD.0 AsmInstructions
SETD.1 ClsEntry
STD.0.1
SETD.0 AsmInstructionCount
LDA.0
SETD.0 ClsLeft
STA.0
clsFindLoop:
SETD.1 ClsEntry
LDD.0.1
INCD.0
INCD.0 ; Past the opcode and the shape, to the name.
SETD.1 ClsName
CALL clsSameName
BRQ clsFindGot
; Seven bytes to an entry: an opcode, a shape, and four characters with a zero.
INIA 0d7
SETD.0 ClsEntry
CALL numAddByte
SETD.0 ClsLeft
LDA.0
DECA
STA.0
BNA clsFindLoop
RSTA
INIB 0d1
CCF
ADD
RET
clsFindGot:
SETD.1 ClsEntry
LDD.0.1
LDA.0
SETD.1 ClsOpcode
STA.1
SETD.1 ClsEntry
LDD.0.1
INCD.0
LDA.0
SETD.1 ClsShape
STA.1
RSTA
RSTB
CCF
ADD
RET
; Four characters at DP0 against four at DP1. Q is zero if they are the same.
clsSameName:
INIA 0d4
SETD.2 ClsLeft2
STA.2
clsSameLoop:
LDA.0
LDB.1
XOR
BNQ clsSameDone
INCD.0
INCD.1
SETD.2 ClsLeft2
LDA.2
DECA
STA.2
BNA clsSameLoop
clsSameDone:
RET
; ---- Literal values ----
; Is TokText a well formed literal? Q is zero if it is, and ClsValue is what it comes to.
; Anything beginning with a zero has to be one, so a failure here is an error rather than
; an invitation to try the next test.
clsValue:
SETD.0 TokText
INCD.0
LDA.0
INIB 0x78 ; 'x'
XOR
BRQ clsValueHex
SETD.0 TokText
INCD.0
LDA.0
INIB 0x64 ; 'd'
XOR
BRQ clsValueDecimal
SETD.0 BadPrefix
CALL clsComplain
BRI clsValueNo
clsValueHex:
INIA 0d16
SETD.0 ClsBase
STA.0
BRI clsValueDigits
clsValueDecimal:
INIA 0d10
SETD.0 ClsBase
STA.0
clsValueDigits:
SETD.0 TokLength
LDA.0
INIB 0d3
CCF
SUB
BRC clsValueEmpty ; Only the prefix, so there are no digits at all.
RSTA
SETD.0 ClsValue
STA.0
SETD.0 TokText
INCD.0
INCD.0
SETD.1 ClsWalk
STD.0.1
clsValueLoop:
SETD.1 ClsWalk
LDD.0.1
LDA.0
BRA clsValueGood
CALL clsDigit
BNQ clsValueBadDigit
; value = value * base + digit, and anything that will not fit in a byte is refused
; rather than wrapped, because a literal is one byte wherever it goes.
SETD.0 ClsDigitValue
LDA.0
SETD.2 ClsValue
LDB.2
PSHA
SETD.0 ClsBase
LDA.0
CALL clsMultiply
BNQ clsValueTooBig
POPA
SETD.0 ClsProduct
LDB.0
CCF
ADD
BRC clsValueTooBig
MVQA
SETD.0 ClsValue
STA.0
SETD.0 ClsWalk
CALL numStep
BRI clsValueLoop
clsValueGood:
RSTA
RSTB
CCF
ADD
RET
clsValueEmpty:
SETD.0 NoDigits
CALL clsComplain
BRI clsValueNo
clsValueBadDigit:
SETD.0 BadDigit
CALL clsComplain
BRI clsValueNo
clsValueTooBig:
POPA
SETD.0 TooBig
CALL clsComplain
clsValueNo:
RSTA
INIB 0d1
CCF
ADD
RET
; The character in A as a digit in ClsBase, into ClsDigitValue. Q is zero if it is one.
clsDigit:
SETD.0 ClsHold
STA.0
; 0 to 9
INIB 0x30
CCF
SUB
BRC clsDigitNo
SETD.0 ClsHold
LDA.0
INIB 0x3A
CCF
SUB
BRC clsDigitDecimal
; A to F, either case, and only when the base has room for them.
SETD.0 ClsBase
LDA.0
INIB 0d16
XOR
BNQ clsDigitNo
SETD.0 ClsHold
LDA.0
CALL clsUpper
SETD.0 ClsByte
LDA.0
SETD.0 ClsHold
STA.0
INIB 0x41
CCF
SUB
BRC clsDigitNo
SETD.0 ClsHold
LDA.0
INIB 0x47
CCF
SUB
BNC clsDigitNo
SETD.0 ClsHold
LDA.0
INIB 0x37 ; 'A' is ten, so the offset is 0x41 less 10.
CCF
SUB
MVQA
SETD.0 ClsDigitValue
STA.0
BRI clsDigitYes
clsDigitDecimal:
SETD.0 ClsHold
LDA.0
INIB 0x30
CCF
SUB
MVQA
SETD.0 ClsDigitValue
STA.0
; A decimal digit is a hexadecimal one too, so this needs no test of the base.
clsDigitYes:
RSTA
RSTB
CCF
ADD
RET
clsDigitNo:
RSTA
INIB 0d1
CCF
ADD
RET
; B times A into ClsProduct. Q is not zero if it would not fit in a byte, which is the
; only answer a literal can use: there is no wider literal to promote it to.
clsMultiply:
SETD.0 ClsMulLeft
STA.0
RSTA
SETD.0 ClsProduct
STA.0
clsMultiplyLoop:
SETD.0 ClsMulLeft
LDA.0
BRA clsMultiplyDone
DECA
STA.0
SETD.0 ClsProduct
LDA.0
CCF
ADD
BRC clsMultiplyOver
MVQA
SETD.0 ClsProduct
STA.0
BRI clsMultiplyLoop
clsMultiplyDone:
RSTA
RSTB
CCF
ADD
RET
clsMultiplyOver:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Odds and ends ----
; The character in A, folded to upper case, into ClsByte.
clsUpper:
SETD.0 ClsHold
STA.0
INIB 0x61 ; 'a'
CCF
SUB
BRC clsUpperDone
SETD.0 ClsHold
LDA.0
INIB 0x7B ; One past 'z'.
CCF
SUB
BNC clsUpperDone
SETD.0 ClsHold
LDA.0
INIB 0d32
CCF
SUB
MVQA
SETD.0 ClsByte
STA.0
RET
clsUpperDone:
SETD.0 ClsHold
LDA.0
SETD.0 ClsByte
STA.0
RET
; The byte at DP0, offset by A, into ClsByte.
clsIndexByte:
PSHA
PSHD.0
POPB
POPA ; The low byte is on top, the way a pointer is pushed.
SETD.0 ClsWalk
STA.0
INCD.0
STB.0
POPA
SETD.0 ClsWalk
CALL numAddByte
SETD.1 ClsWalk
LDD.0.1
LDA.0
SETD.0 ClsByte
STA.0
RET
; Puts A at DP0 offset by the byte at DP2.
clsPutIndexed:
PSHA
PSHD.0
POPB
POPA
SETD.0 ClsPut
STA.0
INCD.0
STB.0
LDA.2
SETD.0 ClsPut
CALL numAddByte
SETD.1 ClsPut
LDD.0.1
POPA
STA.0
RET
; Says what is wrong, with the file and the line, the way an error ought to.
clsComplain:
SWI osPrintString
SETD.0 InFileText
SWI osPrintString
SETD.0 SrcName
SWI osPrintString
SETD.0 AtLineText
SWI osPrintString
SETD.0 TokLine
LDA.0
INCD.0
LDB.0
SWI osPrintNumber
SETD.0 SaidText
SWI osPrintString
SETD.0 TokText
SWI osPrintString
SETD.0 SaidEnd
SWI osPrintString
RET
#Data
ClsType:
0x00
ClsLength:
0x00
ClsOpcode:
0x00
ClsShape:
0x00
ClsSelectorValue:
0x00 0x00
ClsGiven:
0x00
ClsWanted:
0x00
ClsNameLength:
0x00
ClsName:
#Reserve 0d5
ClsValue:
0x00
ClsBase:
0x00
ClsDigitValue:
0x00
ClsProduct:
0x00
ClsMulLeft:
0x00
ClsHold:
0x00
ClsDigitHold:
0x00
ClsByte:
0x00
ClsLeft:
0x00
ClsLeft2:
0x00
ClsWalk:
0x00 0x00
ClsEntry:
0x00 0x00
ClsPut:
0x00 0x00
InFileText:
" in "
AtLineText:
" at line "
SaidText:
"
it said: "
SaidEnd:
"
"
BadPrefix:
"a literal needs 0x for hexadecimal or 0d for decimal"
NoDigits:
"a literal with no digits after its prefix"
BadDigit:
"that is not a digit in the base the prefix asked for"
TooBig:
"a literal too large to fit in one byte"
TooManySelectors:
"more Data Pointer selectors than that instruction has pointers to name"
BadSelector:
"that does not name a Data Pointer, which run from 0 to 3"
+337
View File
@@ -0,0 +1,337 @@
; 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 LabArena
SETD.1 LabNext
STD.0.1
SETD.0 LabIndex
SETD.1 LabBase
STD.0.1
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 labSame
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
; Q is zero if the strings at DP0 and DP1 are the same, both ending in a zero byte.
labSame:
LDA.0
LDB.1
CCF
SUB
BNQ labSameDone
LDA.0
BRA labSameDone ; They ended together, so they matched all the way.
INCD.0
INCD.1
BRI labSame
labSameDone:
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 a
; single program rather than for CosmOS: raising either is changing a number here, and
; running into one says so rather than writing past the end of the table.
LabLimit:
0x01 0x00
LabRoom:
0x08 0x00
LabNamed:
": "
LabAtLine:
", at line "
LabNewLine:
"
"
LabTwice:
"that label is defined twice"
LabFull:
"too many labels"
LabNoRoom:
"no room left for label names"
LabIndex:
#Reserve 0d1024
LabArena:
#Reserve 0d2048
+101
View File
@@ -0,0 +1,101 @@
; Sixteen bit arithmetic, for an assembler that counts in addresses.
;
; 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:
DPUP.0 0d01
DPUP.2 0d01
LDA.0
LDB.2
CCF
ADD
MVQA
STA.0
DPDN.0 0d01
DPDN.2 0d01
LDA.0
LDB.2
ADD ; Carries in from the low half. Nothing between touches it.
MVQA
STA.0
RET
; Adds the byte in A to the two byte number at DP0.
numAddByte:
DPUP.0 0d01
LDB.0
CCF
ADD
MVQA
STA.0
BNC numAddByteDone
DPDN.0 0d01
LDA.0
INCA
STA.0
numAddByteDone:
RET
; Adds one to the two byte number at DP0.
numStep:
DPUP.0 0d01
LDA.0
INCA
STA.0
BNC numStepDone ; It did not wrap, so the high byte is untouched.
DPDN.0 0d01
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
; The two byte number at DP0 becomes zero.
numZero:
RSTA
STA.0
INCD.0
STA.0
RET
+84
View File
@@ -0,0 +1,84 @@
; The source reader on its own, before anything is built on top of it.
;
; Everything else in the assembler reads its source through srcNext, so a fault in here
; would turn up later as a mysterious wrong byte in an output file. It is worth checking by
; itself, against a file whose contents are already known.
;
; It reads whatever it was told to, prints every character back, and then says how many
; lines went past. The file it is given is deliberately bigger than one block, so the seam
; between one block and the next is crossed rather than assumed.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x2000
start:
SETD.0 Wanted
INIB 0d23
SWI osArgument
SETD.0 Wanted
LDA.0
BRA nothingAsked
SETD.0 Wanted
CALL srcOpen
BNQ noFile
readLoop:
CALL srcNext
BNQ readDone
SETD.0 SrcChar
LDA.0
OUTA 0x00 ; Straight to the console: one character is not a string.
BRI readLoop
readDone:
SETD.0 LinesText
SWI osPrintString
SETD.0 SrcLine
LDA.0
INCD.0
LDB.0
SWI osPrintNumber
SETD.0 NewLine
SWI osPrintString
SWI osExit
nothingAsked:
SETD.0 AskText
SWI osPrintString
SWI osExit
noFile:
SETD.0 NoFileText
SWI osPrintString
SWI osExit
#Data
#Base 0x1000
Wanted:
#Reserve 0d23
LinesText:
"---- lines: "
NewLine:
"
"
AskText:
"say which file
"
NoFileText:
"no such file
"
; The libraries go last, after both segments have been based. An included file that carries
; code brings its own #Program and #Data lines with it, and a #Base has to come before
; anything is in the segment it bases - so the bases are set here and the code arrives after.
#Include numbers.asm
#Include source.asm
+225
View File
@@ -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
+106
View File
@@ -0,0 +1,106 @@
; The instruction set, as the assembler needs to see it.
;
; A SECOND COPY, and it is worth saying why rather than hoping nobody notices. The monitor
; has one of these in cosmos.asm, and the assembler cannot use it: the monitor's copy lives
; in the system's data at an address that moves every time CosmOS is rebuilt, and there is
; no linker to reach it by name. So the assembler carries its own 448 bytes. That is the
; cost of having no libraries, paid where it is cheapest to pay.
;
; Both copies are generated by Tests/instructiontable.py from the C assembler's own list,
; and Tests/docs.sh checks both against it. Neither can drift without the suite saying so.
;
; Seven bytes an entry: the opcode, the shape, and four characters of name with the zero
; the assembler puts after a string. Every mnemonic is four characters or fewer, so a name
; padded to four is an exact match rather than a prefix.
;
; It goes in the DATA Segment, because the assembler has to read it and an instruction can
; only read Data Memory. A table in Program Memory could not be reached by the program
; holding it, except through the memory controller.
#Data
; How many bytes an instruction of each shape runs to, the opcode included. The assembler
; does not use this to size a token - the operand that follows is a token of its own and
; carries its own length - but it is what says an instruction is well formed.
AsmShapeLength:
0d1 0d3 0d2 0d2 0d3 0d4 0d3
; How many Data Pointer selectors an instruction of each shape names. This is what the
; assembler needs: a selector is part of the mnemonic rather than a token after it, so it
; is the one thing about an instruction's length that is not settled by the opcode alone.
;
; 0 no operand 4 a selector and a byte
; 1 an address 5 a selector and an address, which is SETD
; 2 a byte 6 two selectors, which is LDD and STD
; 3 a selector
AsmShapeSelectors:
0d0 0d0 0d0 0d1 0d1 0d1 0d2
AsmInstructionCount:
0d64
AsmInstructions:
0x00 0d0 "ADD "
0x01 0d0 "SUB "
0x02 0d0 "AND "
0x03 0d0 "OR "
0x04 0d0 "XOR "
0x05 0d0 "NOTA"
0x06 0d0 "NOTB"
0x07 0d0 "SHL "
0x08 0d0 "SHR "
0x10 0d1 "BRI "
0x11 0d1 "BRQ "
0x12 0d1 "BRA "
0x13 0d1 "BRB "
0x14 0d1 "BRC "
0x15 0d3 "BRD "
0x1A 0d1 "BNQ "
0x1B 0d1 "BNA "
0x1C 0d1 "BNB "
0x1D 0d1 "BNC "
0x17 0d1 "CALL"
0x18 0d2 "SWI "
0x19 0d0 "RETI"
0x1F 0d0 "RET "
0x20 0d0 "RSTA"
0x21 0d0 "RSTB"
0x22 0d0 "INCA"
0x23 0d0 "INCB"
0x24 0d0 "DECA"
0x25 0d0 "DECB"
0x26 0d2 "INIA"
0x27 0d2 "INIB"
0x28 0d0 "CCF "
0x29 0d0 "MVQA"
0x2A 0d0 "MVQB"
0x2B 0d0 "SIF "
0x2C 0d0 "CIF "
0x30 0d0 "PSHQ"
0x31 0d0 "PSHA"
0x32 0d0 "PSHB"
0x33 0d3 "PSHD"
0x34 0d0 "POPA"
0x35 0d0 "POPB"
0x36 0d3 "POPD"
0x40 0d3 "INCD"
0x41 0d3 "DECD"
0x42 0d3 "LDA "
0x43 0d3 "LDB "
0x44 0d3 "STQ "
0x45 0d3 "STA "
0x46 0d3 "STB "
0x47 0d5 "SETD"
0x48 0d4 "DPUP"
0x49 0d4 "DPDN"
0x4A 0d6 "LDD "
0x4B 0d6 "STD "
0x4C 0d3 "MVSD"
0x4D 0d3 "MVDS"
0xD0 0d2 "OUTQ"
0xD1 0d2 "OUTA"
0xD2 0d2 "OUTB"
0xE0 0d2 "INA "
0xE1 0d2 "INB "
0xF0 0d0 "NOP "
0xFF 0d0 "HALT"
+249
View File
@@ -0,0 +1,249 @@
; Tokens out of characters.
;
; A token is a run of characters with whitespace or a comment on either side, or anything
; between a pair of quotes. That is the whole of the lexical grammar: SplitBit assembly has
; no operators, no punctuation and no line continuation, so there is nothing here that has
; to look ahead more than one character.
;
; ONE CHARACTER OF LOOKAHEAD, and it is held here rather than in the reader. A word ends
; when something that is not part of it turns up, and that something has already been read
; by the time anyone knows - so it is put in TokPending and taken again next time. Keeping
; it at this level rather than pushing it back into the reader means the line number needs
; no arithmetic: srcNext counted the newline when it handed it out, and it stays counted.
;
; Zero means "nothing held", which is safe because a source file is text and a text file
; has no zero bytes in it. A file that did would be rejected as unassemblable long before
; the difference showed.
;
; Written by Anachronaut
#Program
; The next token, into TokText with a zero after it. Q is zero if there was one.
;
; TokLength is how long it is, TokString says whether it arrived in quotes, and TokLine is
; the line it STARTED on - captured before the token is read, because a token ending in a
; newline has already moved the reader on to the next line by the time it is finished.
tokNext:
RSTA
SETD.0 TokString
STA.0
tokSkip:
CALL tokGet
BNQ tokEnded
CALL tokIsSpace
BRQ tokSkip
SETD.0 TokChar
LDA.0
INIB 0x3B ; A semicolon starts a comment.
XOR
BNQ tokBegin
tokComment:
CALL tokGet
BNQ tokEnded
SETD.0 TokChar
LDA.0
INIB 0x0A
XOR
BNQ tokComment ; Everything up to the newline belongs to the comment.
BRI tokSkip
tokBegin:
; Where it starts, for anything that has to complain about it later.
SETD.0 TokLine
SETD.2 SrcLine
CALL numSet
RSTA
SETD.0 TokLength
STA.0
SETD.0 TokText
SETD.1 TokPointer
STD.0.1
SETD.0 TokChar
LDA.0
INIB 0x22 ; A quote starts a string.
XOR
BNQ tokWord
INIA 0d1
SETD.0 TokString
STA.0
tokStringLoop:
CALL tokGet
BNQ tokDone ; The file ended inside a string. Take what there is; the
; classifier will have something to complain about.
SETD.0 TokChar
LDA.0
INIB 0x22
XOR
BRQ tokDone
CALL tokAppend
BRI tokStringLoop
tokWord:
CALL tokAppend
tokWordLoop:
CALL tokGet
BNQ tokDone
CALL tokIsSpace
BRQ tokHoldDone
SETD.0 TokChar
LDA.0
INIB 0x3B
XOR
BRQ tokHoldDone ; A comment butting straight up against a word ends it.
CALL tokAppend
BRI tokWordLoop
tokHoldDone:
; Whatever ended the word was not part of it, so it goes back to be looked at again.
SETD.0 TokChar
LDA.0
SETD.0 TokPending
STA.0
tokDone:
SETD.1 TokPointer
LDD.0.1
RSTA
STA.0 ; The zero that makes it a string.
RSTA
RSTB
CCF
ADD ; Q is zero: there was a token.
RET
tokEnded:
RSTA
SETD.0 TokLength
STA.0
SETD.0 TokText
STA.0
RSTA
INIB 0d1
CCF
ADD ; Q is not zero: the source is finished.
RET
; The next character, into TokChar. Q is zero if there was one. Takes the held one first.
tokGet:
SETD.0 TokPending
LDA.0
BRA tokGetFresh
SETD.0 TokChar
STA.0
RSTA
SETD.0 TokPending
STA.0
RSTA
RSTB
CCF
ADD
RET
tokGetFresh:
CALL srcNext
BNQ tokGetNone
SETD.0 SrcChar
LDA.0
SETD.0 TokChar
STA.0
RSTA
RSTB
CCF
ADD
RET
tokGetNone:
RSTA
INIB 0d1
CCF
ADD
RET
; Adds TokChar to the token being built, unless it is already as long as one may be.
;
; A token that runs over is truncated rather than refused, and the classifier refuses it
; afterwards: nothing 255 characters long is a valid mnemonic, literal or label, so the
; error that comes out names what was wrong with it rather than only how long it was.
tokAppend:
SETD.0 TokLength
LDA.0
INIB 0xFF
XOR
BRQ tokAppendFull
SETD.1 TokPointer
LDD.0.1
SETD.2 TokChar
LDA.2
STA.0
INCD.0
STD.0.1
SETD.0 TokLength
LDA.0
INCA
STA.0
tokAppendFull:
RET
; Q is zero if TokChar is whitespace: a space, or anything in the run from tab to carriage
; return, which is what the C library calls a space and what the other assembler uses.
tokIsSpace:
SETD.0 TokChar
LDA.0
INIB 0x20
XOR
BRQ tokSpaceYes
SETD.0 TokChar
LDA.0
INIB 0x09
CCF
SUB
BRC tokSpaceNo ; Below a tab.
SETD.0 TokChar
LDA.0
INIB 0x0E
CCF
SUB
BNC tokSpaceNo ; Past a carriage return.
tokSpaceYes:
RSTA
RSTB
CCF
ADD
RET
tokSpaceNo:
RSTA
INIB 0d1
CCF
ADD
RET
#Data
TokLine:
0x00 0x00
TokPointer:
0x00 0x00
TokLength:
0x00
TokString:
0x00
TokChar:
0x00
TokPending:
0x00
; As long as a token may be, and one more for the zero. The other assembler stops at the
; same 255, and the limit is worth matching rather than choosing again.
TokText:
#Reserve 0d256
+178
View File
@@ -0,0 +1,178 @@
; The assembler's front end on its own, one line per token.
;
; Prints the line each token started on, what the token turned out to be, how many bytes
; it will come to, and the token itself between brackets so that whitespace at either end
; would show if any ever leaked in.
;
; WHAT A TOKEN IS is the part worth checking here rather than at the far end. A wrong
; classification does not produce a wrong byte in an obvious place - it produces a right
; looking program of the wrong length, with everything after it shifted, and by then the
; only symptom is that a label points at the middle of an instruction.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x2000
start:
SETD.0 Wanted
INIB 0d23
SWI osArgument
SETD.0 Wanted
LDA.0
BRA nothingAsked
SETD.0 Wanted
CALL srcOpen
BNQ noFile
tokenLoop:
CALL tokNext
BNQ tokensDone
SETD.0 TokLine
LDA.0
INCD.0
LDB.0
SWI osPrintNumber
CALL clsToken
BNQ badToken
; Which of the six it turned out to be. The names are four characters and a space, so
; the columns line up without any counting.
SETD.0 TypeNames
SETD.2 ClsType
LDA.2
CALL nameOfType
SETD.1 NamePointer
LDD.0.1
SWI osPrintString
SETD.0 ClsLength
LDA.0
RSTB
PSHA
POPB
RSTA
SWI osPrintNumber
SETD.0 OpenMark
SWI osPrintString
SETD.0 TokText
SWI osPrintString
SETD.0 CloseMark
SWI osPrintString
BRI tokenLoop
badToken:
SETD.0 StoppedText
SWI osPrintString
SWI osExit
; The name of type A, out of a table of fixed width entries so that no pointer arithmetic
; is needed beyond a multiply by the width.
nameOfType:
SETD.0 TypeWidth
LDB.0
RSTA
SETD.0 NameLeft
STA.0
SETD.2 ClsType
LDA.2
SETD.0 NameOffset
CALL numZero
nameLoop:
SETD.2 ClsType
LDA.2
SETD.0 NameLeft
LDB.0
CCF
SUB
BRQ nameFound
SETD.0 TypeWidth
LDA.0
SETD.0 NameOffset
CALL numAddByte
SETD.0 NameLeft
LDA.0
INCA
STA.0
BRI nameLoop
nameFound:
; The answer is left in NamePointer rather than in DP0, because a RET puts Data Pointers
; 0 to 2 back as they were: a routine cannot hand back a pointer, only write one down.
SETD.0 TypeNames
SETD.1 NamePointer
STD.0.1
SETD.0 NamePointer
SETD.2 NameOffset
CALL numAdd
RET
tokensDone:
SETD.0 DoneText
SWI osPrintString
SWI osExit
nothingAsked:
SETD.0 AskText
SWI osPrintString
SWI osExit
noFile:
SETD.0 NoFileText
SWI osPrintString
SWI osExit
#Data
#Base 0x1000
Wanted:
#Reserve 0d23
OpenMark:
" ["
CloseMark:
"]
"
; Six names of eleven characters each, counting the zero the assembler puts on the end of
; every string. Written one to a line so that adding a type is adding a line.
TypeNames:
" keyword "
" instr "
" value "
" string "
" label: "
" label "
TypeWidth:
0d11
NameLeft:
0x00
NameOffset:
0x00 0x00
NamePointer:
0x00 0x00
StoppedText:
"---- stopped: the assembler does not understand that
"
DoneText:
"---- no more tokens
"
AskText:
"say which file
"
NoFileText:
"no such file
"
#Include numbers.asm
#Include source.asm
#Include token.asm
#Include classify.asm
#Include table.asm
+6
View File
@@ -184,6 +184,12 @@ the services CosmOS provides. The currently installed services are:
| `osFileInfo` | DP0 names a file; Q reports whether it is there and DP3 returns how many blocks it occupies. |
| `osFileBlock` | DP0 names a file, DP1 a destination, and A with B give which block; Q reports success and DP3 returns how many of the block's bytes belong to the file. |
The largest application CosmOS has is the assembler in `Programs/CosmOS/Assembler/`, which
is why the streaming services below exist: it reads source a block at a time, twice, and
keeps only its label table in between. It travels with CosmOS rather than with the emulator,
for the same reason the C assembler travels with the emulator - it is part of the system it
is written for.
`osFileInfo` and `osFileBlock` are how an application reads a file too big to hold. A whole
file arrives through `osFileRead`, which cannot help with anything above 64K, and CosmOS's
own source is above it. Neither call keeps anything open: each one names the file and says