M2: the native assembler builds applications
> load Asm.sbx
> run Say.asm
wrote Say.sbx: program 46, data 93, labels 7
> load Say.sbx
> run built by the machine itself
it says: built by the machine itself
The machine assembles an application and then runs what it built. Say,
greet and Files all come out byte for byte identical to the C assembler's,
and Tests/native.sh checks all three on every run alongside the boot image
M1 already covered.
WHAT IT TOOK, and it was more than #Include and #Base:
#Include The reader is a stack of readers. The current file's whole
state goes aside - buffer and all, 292 bytes - the new one
opens, and the end of it pops the old one back. A file goes in
once; including it twice does nothing, which is what lets two
libraries depend on a third. The list is forgotten between the
passes, because the second has to walk the same tree.
#Base Cursors start there, so labels hold the addresses the program
will really have. A program that says where it goes gets the
SBEX header and a .sbx name; one that says nothing gets SPBT
and .bin. A program that bases one segment and leaves the
other unbased with content in it is refused.
#Reserve Runs of zeroes, moved over in the first pass and written in
#Align the second. How many an #Align comes to depends on where the
cursor has reached, which is why both passes keep a cursor.
#Vectors Names are read and numbered, pinned where the source pins
them, so SWI osPrintString resolves. Every application needs
this - a program that calls a service names a vector declared
in a file it includes.
THE TWO PASSES ARE NOW ONE LOOP, walked twice, with Emitting the only
difference. They have to agree about the length of every token, and the way
they stop agreeing is by being two pieces of code that drifted apart -
which is the exact shape of the bug this assembler found in the C one.
Sharing the body means there is nothing to drift. What is left is checked
anyway: the second pass compares its own totals against the first's and
refuses to write the file if they differ.
THE BUG WORTH RECORDING. The tokenizer holds one character of lookahead,
and at an #Include that character belongs to the file being put aside. It
was carried across and handed back on the way out, which is wrong: a file
runs out in the middle of whatever the tokenizer happens to be doing, so
the character arrives in the middle of a word. `start:` came back as `s`
and then `tart:` - and the result assembled into a perfectly plausible
file. The fix is to undo the read instead, so the character is simply still
there when the file is opened again and the question of when to hand it
back never arises.
Three smaller ones, all old friends: three places took the CONTENTS of a
buffer where they wanted its ADDRESS; vecTakeAuto returned its answer in A,
which a RET puts back; and pass two re-declared every vector because only
the label table was being skipped on the second walk.
sameText moved down into numbers.asm from the label table - four parts want
it now, and a reader test that includes neither labels nor tokens has to
build on its own.
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
affe9d09ea
commit
c5e4ec3455
@@ -12,6 +12,24 @@
|
||||
; 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.
|
||||
;
|
||||
; ---- A stack of readers ----
|
||||
;
|
||||
; #Include splices another file in where it stands, so the reader is a stack: srcInclude
|
||||
; puts the current file's whole state aside, opens the new one, and the end of that file
|
||||
; pops the old one back and carries on where it left off. Everything above works on "the
|
||||
; current file" and does not know the stack is there.
|
||||
;
|
||||
; THE WHOLE STATE GOES ASIDE, buffer and all, 293 bytes of it. Keeping only the position
|
||||
; and re-reading the block on the way back would be cheaper in memory and would cost a disk
|
||||
; read per pop; at six levels of nesting the copy costs less than the arithmetic to avoid it.
|
||||
; The buffer pointer survives the trip because it points into the buffer, which is always at
|
||||
; the same address - the state is saved from and restored to the same variables.
|
||||
;
|
||||
; A FILE IS INCLUDED ONCE. Including it twice is not an error, it just does nothing, which
|
||||
; is what lets two libraries depend on a third. The names are remembered for the length of
|
||||
; one pass and forgotten between them, because the second pass has to walk exactly the same
|
||||
; tree the first one did.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Program
|
||||
@@ -22,6 +40,20 @@
|
||||
; 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 SrcTopName
|
||||
CALL srcKeepName ; Kept apart, so that each pass can open it again.
|
||||
CALL srcRestart
|
||||
RET
|
||||
|
||||
; Back to the top of the tree: the first file, no includes taken yet, nothing on the stack.
|
||||
; This is what starts each pass.
|
||||
srcRestart:
|
||||
RSTA
|
||||
SETD.0 SrcDepth
|
||||
STA.0
|
||||
SETD.0 IncCount
|
||||
STA.0
|
||||
SETD.0 SrcTopName
|
||||
SETD.1 SrcName
|
||||
CALL srcKeepName
|
||||
CALL srcRewind
|
||||
@@ -118,13 +150,262 @@ srcNextDone:
|
||||
RET
|
||||
|
||||
srcAtEnd:
|
||||
; This file is finished. If it was included by another, that one is not: it goes back on
|
||||
; and the next character comes from where it left off, which is what makes an include
|
||||
; read as though the text had been written there.
|
||||
INIA 0d1
|
||||
SETD.0 SrcEnded
|
||||
STA.0
|
||||
SETD.0 SrcDepth
|
||||
LDA.0
|
||||
BRA srcNothingLeft
|
||||
CALL srcPop
|
||||
BRI srcNext
|
||||
|
||||
srcNothingLeft:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD ; Q is not zero: the file is finished.
|
||||
ADD ; Q is not zero: there is no more source anywhere.
|
||||
RET
|
||||
|
||||
; ---- The stack ----
|
||||
|
||||
; Opens the file named at DP0 as though its text were written here. Q is zero if the
|
||||
; reader is now inside it, or if it had already been included and there is nothing to do.
|
||||
srcInclude:
|
||||
SETD.1 IncWanted
|
||||
CALL srcKeepName
|
||||
CALL srcSeenAlready
|
||||
BRQ srcIncludeSkip
|
||||
|
||||
SETD.0 SrcDepth
|
||||
LDA.0
|
||||
SETD.2 SrcDepthLimit
|
||||
LDB.2
|
||||
CCF
|
||||
SUB
|
||||
BNC srcTooDeep
|
||||
|
||||
CALL srcRemember
|
||||
CALL srcPush
|
||||
SETD.0 IncWanted
|
||||
SETD.1 SrcName
|
||||
CALL srcKeepName
|
||||
CALL srcRewind
|
||||
BNQ srcIncludeGone
|
||||
RET ; Q is zero, out of srcRewind.
|
||||
|
||||
srcIncludeSkip:
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD ; Already in, so this line does nothing at all.
|
||||
RET
|
||||
|
||||
srcIncludeGone:
|
||||
; The file is not there. The stack is left as it is: the caller stops the assembly, and
|
||||
; unwinding for the sake of tidiness would only hide where it happened.
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
srcTooDeep:
|
||||
SETD.0 TooDeepText
|
||||
SWI osPrintString
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; Puts the current file aside and makes room for another.
|
||||
srcPush:
|
||||
CALL srcSlot
|
||||
SETD.0 SrcState
|
||||
SETD.1 SrcSlot
|
||||
LDD.1.1
|
||||
CALL srcCopyState
|
||||
SETD.0 SrcDepth
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
RET
|
||||
|
||||
; And takes it back.
|
||||
srcPop:
|
||||
SETD.0 SrcDepth
|
||||
LDA.0
|
||||
DECA
|
||||
STA.0
|
||||
CALL srcSlot
|
||||
SETD.1 SrcSlot
|
||||
LDD.0.1
|
||||
SETD.1 SrcState
|
||||
CALL srcCopyState
|
||||
RET
|
||||
|
||||
; Puts one character back, so that the next read produces it again. The character itself
|
||||
; is in A, because whether it was a newline decides whether a line goes back too.
|
||||
;
|
||||
; The tokenizer holds one character of lookahead, and at an #Include that character belongs
|
||||
; to the file about to be put aside. Undoing the read is how it stays with that file: when
|
||||
; the file is opened again the character is simply still there, and nothing has to be
|
||||
; carried across the include or handed back at some moment chosen by the reader.
|
||||
;
|
||||
; CARRYING IT ACROSS WAS THE OBVIOUS THING AND IT WAS WRONG. A file runs out in the middle
|
||||
; of whatever the tokenizer happens to be doing, so handing the character back then injects
|
||||
; it into the middle of a word: `start:` came back as `s` and then `tart:`, which assembles
|
||||
; into a file that looks entirely reasonable.
|
||||
srcStepBack:
|
||||
INIB 0x0A
|
||||
XOR
|
||||
BNQ srcStepBackAt
|
||||
SETD.0 SrcLine
|
||||
SETD.2 SrcOne
|
||||
CALL numTake ; A newline not yet read has not started a line either.
|
||||
|
||||
srcStepBackAt:
|
||||
SETD.0 SrcAt
|
||||
SETD.2 SrcOne
|
||||
CALL numTake
|
||||
SETD.0 SrcPointer
|
||||
SETD.2 SrcOne
|
||||
CALL numTake
|
||||
RET
|
||||
|
||||
; Where the slot for the current depth is, into SrcSlot.
|
||||
srcSlot:
|
||||
SETD.0 SrcStack
|
||||
SETD.1 SrcSlot
|
||||
STD.0.1 ; WHERE the stack is, not what is in it.
|
||||
SETD.0 SrcSlotLeft
|
||||
SETD.2 SrcDepth
|
||||
LDA.2
|
||||
STA.0
|
||||
srcSlotLoop:
|
||||
SETD.0 SrcSlotLeft
|
||||
LDA.0
|
||||
BRA srcSlotDone
|
||||
DECA
|
||||
STA.0
|
||||
SETD.0 SrcSlot
|
||||
SETD.2 SrcStateBytes
|
||||
CALL numAdd
|
||||
BRI srcSlotLoop
|
||||
srcSlotDone:
|
||||
RET
|
||||
|
||||
; The whole reader state, from DP0 to DP1.
|
||||
srcCopyState:
|
||||
SETD.2 SrcCopyFrom
|
||||
STD.0.2
|
||||
SETD.2 SrcCopyTo
|
||||
STD.1.2
|
||||
SETD.0 SrcCopyLeft
|
||||
SETD.2 SrcStateBytes
|
||||
CALL numSet
|
||||
srcCopyLoop:
|
||||
SETD.1 SrcCopyFrom
|
||||
LDD.0.1
|
||||
LDA.0
|
||||
SETD.1 SrcCopyTo
|
||||
LDD.0.1
|
||||
STA.0
|
||||
SETD.0 SrcCopyFrom
|
||||
CALL numStep
|
||||
SETD.0 SrcCopyTo
|
||||
CALL numStep
|
||||
SETD.0 SrcCopyLeft
|
||||
SETD.2 SrcOne
|
||||
CALL numTake
|
||||
SETD.0 SrcCopyLeft
|
||||
LDA.0
|
||||
INCD.0
|
||||
LDB.0
|
||||
OR
|
||||
BNQ srcCopyLoop
|
||||
RET
|
||||
|
||||
; ---- Which files have been in ----
|
||||
|
||||
; Q is zero if IncWanted has already been included in this pass.
|
||||
srcSeenAlready:
|
||||
RSTA
|
||||
SETD.0 IncLeft
|
||||
STA.0
|
||||
srcSeenLoop:
|
||||
SETD.0 IncLeft
|
||||
LDA.0
|
||||
SETD.2 IncCount
|
||||
LDB.2
|
||||
CCF
|
||||
SUB
|
||||
BRQ srcSeenNo
|
||||
CALL srcSeenSlot
|
||||
SETD.1 IncSlot
|
||||
LDD.0.1
|
||||
SETD.1 IncWanted
|
||||
CALL sameText
|
||||
BRQ srcSeenYes
|
||||
SETD.0 IncLeft
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
BRI srcSeenLoop
|
||||
srcSeenYes:
|
||||
RSTA
|
||||
RSTB
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
srcSeenNo:
|
||||
RSTA
|
||||
INIB 0d1
|
||||
CCF
|
||||
ADD
|
||||
RET
|
||||
|
||||
; Writes IncWanted down as having been included.
|
||||
srcRemember:
|
||||
SETD.0 IncLeft
|
||||
SETD.2 IncCount
|
||||
LDA.2
|
||||
STA.0
|
||||
CALL srcSeenSlot
|
||||
SETD.0 IncWanted
|
||||
SETD.1 IncSlot
|
||||
LDD.1.1
|
||||
CALL srcKeepName
|
||||
SETD.0 IncCount
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
RET
|
||||
|
||||
; Where name number IncLeft sits, into IncSlot. Fixed fields of 23 bytes: there are few of
|
||||
; these and they are short, so an arena would cost more code than it saved.
|
||||
srcSeenSlot:
|
||||
SETD.0 IncNames
|
||||
SETD.1 IncSlot
|
||||
STD.0.1
|
||||
SETD.0 IncSlotLeft
|
||||
SETD.2 IncLeft
|
||||
LDA.2
|
||||
STA.0
|
||||
srcSeenSlotLoop:
|
||||
SETD.0 IncSlotLeft
|
||||
LDA.0
|
||||
BRA srcSeenSlotDone
|
||||
DECA
|
||||
STA.0
|
||||
INIA 0d23
|
||||
SETD.0 IncSlot
|
||||
CALL numAddByte
|
||||
BRI srcSeenSlotLoop
|
||||
srcSeenSlotDone:
|
||||
RET
|
||||
|
||||
; Fetches the block SrcIndex names, and steps SrcIndex past it. Q is zero if there was one.
|
||||
@@ -199,6 +480,9 @@ srcKeepEnd:
|
||||
|
||||
#Data
|
||||
|
||||
; ---- The current file, as one block so that it can be put aside in one piece ----
|
||||
;
|
||||
SrcState:
|
||||
SrcName:
|
||||
#Reserve 0d23
|
||||
SrcBlocks:
|
||||
@@ -215,11 +499,60 @@ 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
|
||||
|
||||
; 292 bytes: a name of 23, six numbers of two, one single byte, and the buffer. NOTHING MAY
|
||||
; BE ADDED IN THE MIDDLE OF THE BLOCK ABOVE without changing this to match.
|
||||
SrcStateBytes:
|
||||
0x01 0x24
|
||||
SrcDepthLimit:
|
||||
0d6
|
||||
SrcOne:
|
||||
0x00 0x01
|
||||
|
||||
SrcChar:
|
||||
0x00
|
||||
SrcLeft:
|
||||
0x00
|
||||
SrcDepth:
|
||||
0x00
|
||||
SrcSlot:
|
||||
0x00 0x00
|
||||
SrcSlotLeft:
|
||||
0x00
|
||||
SrcCopyFrom:
|
||||
0x00 0x00
|
||||
SrcCopyTo:
|
||||
0x00 0x00
|
||||
SrcCopyLeft:
|
||||
0x00 0x00
|
||||
|
||||
; The file the assembly started from, so that each pass can open it again.
|
||||
SrcTopName:
|
||||
#Reserve 0d23
|
||||
|
||||
IncWanted:
|
||||
#Reserve 0d23
|
||||
IncCount:
|
||||
0x00
|
||||
IncLeft:
|
||||
0x00
|
||||
IncSlot:
|
||||
0x00 0x00
|
||||
IncSlotLeft:
|
||||
0x00
|
||||
|
||||
TooDeepText:
|
||||
"included files are nested deeper than this assembler will follow
|
||||
"
|
||||
|
||||
; Six levels of nesting, at 293 bytes each. CosmOS itself nests three deep.
|
||||
SrcStack:
|
||||
#Reserve 0d1758
|
||||
|
||||
; Sixteen names of 23 bytes, which is more separate files than anything here includes.
|
||||
IncNames:
|
||||
#Reserve 0d368
|
||||
|
||||
Reference in New Issue
Block a user