S2: the assembler writes the file as it makes it

The output image is gone. It was eighteen kilobytes and it is now one block of
window, because the file was always produced in order and only ever needed to
be written that way.

Everything works in FILE OFFSETS now. A cursor is a two byte number counting
from the front of the file, and since a block is two hundred and fifty six
bytes, the block it lands in is the offset's high byte and the place within that
block is its low one - so there is no division anywhere, and ImgWalk, ProgPut
and DataPut needed no change but where they start.

ONE WINDOW RATHER THAN THREE. The plan said three: one per segment, and a third
for the block where the program ends and the data begins, which belongs to both.
Fetching a block back instead makes all of that one case. The header is patched
after every byte is out, the boundary block is written by both cursors, and both
are simply revisits - a revisit is what fetching handles. osFileFetch is the
service that allows it, and is the read side of the write.

A run of bytes in one segment costs nothing extra; a switch between segments
costs two block operations, and a source file has a few dozen switches and
several thousand bytes.

Two bugs, both a pointer meaning two things:

putAt took the cursor to advance in DP2 and then wanted DP2 for the window's
address. A call puts DP2 back the way it was AT THE CALL, so the step at the end
moved whatever the last call had left there - the window walked off across
memory while the cursor stood still. It goes in memory now, like the block did
in S1, and for the same reason.

The size the file is created at could not be right. How many vectors are
actually installed is not known until the second pass has resolved their
handlers, and by then the file must already exist to be written into - so Keys,
which brings one vector, came out four bytes short. Teaching the first pass to
count them meant teaching it about devices, and about a Boot line in a loadable
program not being installed at all, which is two ways to disagree with the
second pass about what a file contains.

So osFileDone is told the size instead. A writer asks for as much as the file
could possibly come to - the whole of it plus four bytes for every vector
DECLARED, which no file can exceed - and says what it really came to at the end.
The blocks it did not use go back to the free count. Asking for too much costs a
moment; asking for too little writes off the end of a file.

That is a better service for it, not a workaround. A writer that cannot know its
size until the last byte is the ordinary case, and it is exactly the case this
whole rung exists for.

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-25 16:58:17 -04:00
co-authored by Claude Opus 5
parent 9f7dffdeca
commit fb7b224bbb
8 changed files with 505 additions and 46 deletions
+318 -33
View File
@@ -57,6 +57,7 @@ start:
CALL settleFormat
CALL deriveName ; After the first pass: what it is called depends on whether a
; #Base turned up, and that is not known until then.
CALL layOutImage
BNQ stopped
CALL passTwo
@@ -1360,8 +1361,7 @@ layOutImage:
BNQ layOutNo
SETD.0 ImgWalk
SETD.2 ScratchImage
CALL numSet
CALL numZero ; The front of the file, not the front of a buffer.
SETD.0 MagicSPBT
INIA 0d4
CALL putBytes
@@ -1414,8 +1414,7 @@ layOutLoadable:
BNQ layOutNo
SETD.0 ImgWalk
SETD.2 ScratchImage
CALL numSet
CALL numZero ; The front of the file, not the front of a buffer.
SETD.0 MagicSBEX
INIA 0d4
CALL putBytes
@@ -1460,17 +1459,104 @@ layOutNo:
ADD
RET
; ---- Room for the file ----
;
; Nothing is held in memory any more, so what is checked here is not a buffer. It is that
; the SIZE CAN STILL BE DESCRIBED: every number in this assembler is two bytes, so a file
; of more than 65,535 would have wrapped round while it was being added up and come out
; smaller than one of its own segments. A wrapped total would allocate a short file and
; write off the end of it.
;
; Then the file is started, which is where running out of DISK is found - and that happens
; before a single byte is written, so a refusal costs nothing that was already there.
checkImageRoom:
SETD.0 ImgRoom
SETD.2 ImgTotal
SETD.0 ImgTotal
SETD.2 ImgProgLen
CALL numCompare
BRC imageTooBig ; The total is smaller than a part of it: it went round.
SETD.0 ImgTotal
SETD.2 ImgDataLen
CALL numCompare
BRC imageTooBig
; ---- How much room to ask for ----
;
; MORE THAN THE FILE WILL COME TO, on purpose. How many vectors are actually installed is
; not known until the second pass has resolved every handler, and by then the file has to
; exist to be written into. What IS known now is how many vectors were DECLARED, and no
; more than that can be installed - so the room asked for is the whole file plus four
; bytes for each of them and five for a marker.
;
; Asking for too much costs nothing but a moment: osFileDone is told what it really came
; to and the difference goes back. Asking for too little would have meant writing off the
; end of the file, and the first sign of it was Keys coming out four bytes short.
SETD.0 ImgAsk
SETD.2 ImgTotal
CALL numSet
SETD.0 ImgVecMost
SETD.2 VecCount
CALL numSet
SETD.0 ImgVecMost
SETD.2 ImgVecMost
CALL numAdd
SETD.0 ImgVecMost
SETD.2 ImgVecMost
CALL numAdd ; Four bytes an entry.
SETD.0 ImgAsk
SETD.2 ImgVecMost
CALL numAdd
INIA 0d5
SETD.0 ImgAsk
CALL numAddByte
; Blocks are the high half of the size and the tail is the low half, which is how an
; entry holds one and why nothing here has to divide.
SETD.1 ImgAsk
LDA.1
SETD.0 ImgOutBlocks
STA.0 ; The high half is whole blocks.
SETD.1 ImgAsk
INCD.1
LDA.1
SETD.0 ImgOutTail
STA.0 ; And the low half is what is left over.
; DP3 wants the block count and A the tail. The Stack takes the high byte first, which is
; the way every reader in the system takes a count back out of DP3.
RSTA
PSHA
SETD.0 ImgOutBlocks
LDA.0
PSHA
POPD.3
SETD.0 ImgOutTail
LDA.0
SETD.0 OutName
SWI osFileStart
BNQ imageNoRoom
; Nothing in the window yet.
RSTA
SETD.0 OutHeld
STA.0
SETD.0 OutDirty
STA.0
RSTA
RSTB
CCF
ADD
RET
imageNoRoom:
SETD.0 NoRoomText
SWI osPrintString
RSTA
INIB 0d1
CCF
ADD
RET
imageTooBig:
SETD.0 TooBigText
SWI osPrintString
@@ -1481,16 +1567,171 @@ imageTooBig:
RET
; Puts A down at ImgWalk and steps it.
putByte:
; ---- Putting a byte into the file ----
;
; THE FILE IS WRITTEN AS IT IS MADE, through one block of window, rather than being built
; whole in memory and handed over at the end. Everything below works in FILE OFFSETS: a
; cursor is a two byte number counting from the front of the file, and because a block is
; two hundred and fifty six bytes, the block it lands in is the offset's HIGH byte and the
; place within that block is its LOW one. No division anywhere, which is just as well.
;
; There are two cursors and they move independently, because #Program and #Data alternate
; in source and the two segments are consecutive in the file. Whenever a byte lands in a
; block other than the one in hand, the one in hand goes back to the disk and the new one
; is fetched - so a run of bytes in one segment costs nothing extra, and a switch between
; segments costs two block operations. There are a few dozen switches in a source file and
; several thousand bytes.
;
; The block is FETCHED rather than assumed blank, and that is what makes the whole thing
; work with one window instead of three. The header is patched after every byte is out; the
; block where the program ends is the same block the data begins in; both are simply
; revisits, and a revisit is what fetching handles.
; A at the file offset in the two byte number DP2 names, which is then stepped on.
putAt:
SETD.0 ImgHold
STA.0
SETD.1 ImgWalk
LDD.0.1
SETD.2 ImgHold
LDA.2
STA.0
; WHICH CURSOR THIS IS, KEPT IN MEMORY. It arrives in DP2 and everything below wants DP2
; for something of its own - and a call puts DP2 back the way it was AT THE CALL, not the
; way it was on the way in. Leaving it there meant the step at the end moved whatever the
; last call had left in DP2, which was the address of the window, and the window walked
; off across memory while the cursor stood still.
SETD.0 ImgCursor
STD.2.0
; Which block, and where in it: a block is two hundred and fifty six bytes, so the two
; halves of the offset are exactly those two things.
PSHD.2
POPD.0
LDA.0
SETD.1 ImgBlockWant
STA.1
INCD.0
STD.0.1
LDA.0
SETD.1 ImgSlot
STA.1
CALL outHold
BNQ putAtFailed
SETD.0 OutAt
SETD.2 ScratchWindow
CALL numSet
SETD.0 OutAt
SETD.1 ImgSlot
LDA.1
CALL numAddByte
SETD.1 OutAt
LDD.0.1
SETD.1 ImgHold
LDA.1
STA.0
INIA 0x01
SETD.0 OutDirty
STA.0
SETD.1 ImgCursor
LDD.0.1
CALL numStep
RSTA
RSTB
CCF
ADD
RET
putAtFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; Makes sure the block ImgBlockWant names is the one in the window, writing back whatever
; was there and fetching the new one.
outHold:
SETD.0 OutHeld
LDA.0
BRA outFetch ; Nothing in hand at all.
SETD.0 OutBlock
LDA.0
SETD.2 ImgBlockWant
LDB.2
CCF
SUB
BRQ outHeldAlready
CALL outFlush
BNQ outHoldNo
outFetch:
SETD.0 ImgBlockWant
LDA.0
SETD.1 OutBlock
STA.1
SETD.1 ScratchWindow
LDD.1.1
SETD.0 OutBlock
LDB.0
RSTA
SWI osFileFetch
BNQ outHoldNo
INIA 0x01
SETD.0 OutHeld
STA.0
RSTA
SETD.0 OutDirty
STA.0
outHeldAlready:
RSTA
RSTB
CCF
ADD
RET
outHoldNo:
RSTA
INIB 0d1
CCF
ADD
RET
; Whatever is in the window goes back to the file, if anything has been put in it.
outFlush:
SETD.0 OutHeld
LDA.0
BRA outFlushNothing
SETD.0 OutDirty
LDA.0
BRA outFlushNothing
SETD.1 ScratchWindow
LDD.1.1
SETD.0 OutBlock
LDB.0
RSTA
SWI osFileWrite
BNQ outHoldNo
RSTA
SETD.0 OutDirty
STA.0
outFlushNothing:
RSTA
RSTB
CCF
ADD
RET
; The header and everything else that is written in order goes through ImgWalk.
putByte:
SETD.2 ImgWalk
CALL putAt
RET
; Puts A bytes from DP0 down at ImgWalk.
@@ -1543,12 +1784,11 @@ emitByte:
emitToProgram:
SETD.1 ProgPut
emitPut:
LDD.0.1
SETD.2 EmitHold
LDA.2
STA.0
INCD.0
STD.0.1
PSHD.1
POPD.2 ; ProgPut or DataPut, whichever this byte belongs to.
SETD.0 EmitHold
LDA.0
CALL putAt
RET
; The byte at DP0 offset by A, into ClsByte. The classifier has one of these; this is the
@@ -1676,8 +1916,8 @@ writeVectorsHeader:
LDA.0
BRA writeVectorsNone
SETD.0 ImgWalk
SETD.2 ScratchImage
CALL numSet
CALL numZero ; Back to the front of the file, which is a revisit like
; any other: the window fetches block nought again.
SETD.0 ImgWalk
INIA 0d4
CALL numAddByte
@@ -1695,8 +1935,8 @@ writeVectorsNone:
LDA.0
BRA writeVectorsOut
SETD.0 ImgWalk
SETD.2 ScratchImage
CALL numSet
CALL numZero ; Back to the front of the file, which is a revisit like
; any other: the window fetches block nought again.
SETD.0 ImgWalk
INIA 0d8
CALL numAddByte
@@ -1772,15 +2012,33 @@ vectorNotEntry:
ADD
RET
; Whatever is still in the window, and then the file takes its name. Nothing that was
; already on the disk has been touched until this last step.
writeImage:
SETD.0 OutName
SETD.1 ScratchImage
LDD.1.1
SETD.2 ImgTotal
LDA.2
INCD.2
LDB.2
SWI osFileSave
CALL outFlush
BNQ writeFailed
; What it really came to, which is what the room was asked for less whatever the vectors
; did not need.
SETD.1 ImgTotal
LDA.1
SETD.0 ImgOutBlocks
STA.0
SETD.1 ImgTotal
INCD.1
LDA.1
SETD.0 ImgOutTail
STA.0
RSTA
PSHA
SETD.0 ImgOutBlocks
LDA.0
PSHA
POPD.3
SETD.0 ImgOutTail
LDA.0
SWI osFileDone
BNQ writeFailed
RSTA
RSTB
@@ -2054,8 +2312,35 @@ DropWalk:
; actually is. Three numbers in three files describe these buffers - this one, LabLimit and
; LabRoom in labels.asm, and the map itself - and each of the three has now been the one
; that was left behind while the other two moved.
ImgRoom:
0x48 0x00
; Which block of the file is in the window, and whether anything has been put in it. The
; window itself is in scratch, like every other buffer here: a #Reserve is written into the
; file as zeroes and copied at load, and a program that carries its own scratch pays for it
; twice.
OutAt:
0x00 0x00
OutBlock:
0x00
OutHeld:
0x00
OutDirty:
0x00
ImgBlockWant:
0x00
ImgSlot:
0x00
ImgOutBlocks:
0x00
ImgOutTail:
0x00
ImgCursor:
0x00 0x00
ImgAsk:
0x00 0x00
ImgVecMost:
0x00 0x00
NoRoomText:
"no room on the disk for the output
"
MagicSPBT:
"SPBT"