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
+30 -2
View File
@@ -987,9 +987,36 @@ handleFileWrite:
STA.2
RETI
; Nothing to be told. The old file goes and the temporary takes its name, which is the only
; step that can lose anything and the last one.
; DP1 is where the block goes, and A and B together say which one, the same way writing is
; told. Reads back a block of the file being written.
handleFileFetch:
SETD.2 SbfsIndex
STA.2
INCD.2
STB.2
CALL sbfsStreamFetch
MVQA
MVSD.2
DPUP.2 0d02
STA.2
RETI
; DP3 is how many whole blocks it came to and A is what is left over, told the same way
; osFileStart is told. It need not be what was asked for: a writer that cannot know its
; size until the last byte asks for enough at the start, where running out costs nothing,
; and says the truth here. The blocks it did not use go back.
;
; This is the only step that can lose anything, and the last one.
handleFileDone:
SETD.2 SbfsFileTail
STA.2
PSHD.3
POPB
POPA
SETD.2 SbfsFileBlocks
STA.2
INCD.2
STB.2
CALL fileForget
CALL sbfsStreamDone
MVQA
@@ -3744,6 +3771,7 @@ CommandLine:
osFileStart handleFileStart
osFileWrite handleFileWrite
osFileDone handleFileDone
osFileFetch handleFileFetch
osPrintNumber handlePrintNumber
osBreak handleBreak
Device 0x20 diskDone
+110 -1
View File
@@ -2420,11 +2420,60 @@ sbfsStreamWrite:
CALL sbfsWriteOne
RET
; DP1 is where the block should go and SbfsIndex says which one. The other direction of
; sbfsStreamWrite, and the thing that lets a writer keep only ONE block of a file in hand:
; anything writing two parts of a file at once has to be able to put a block down, go
; somewhere else, and pick it up again where it left off.
sbfsStreamFetch:
SETD.0 SbfsStreamOpen
LDA.0
BRA sbfsStreamNo
SETD.0 SbfsStreamTo
STD.1.0
SETD.0 SbfsFileStart
SETD.2 SbfsStreamAt
CALL sbfsSetWord
SETD.0 SbfsFileBlocks
SETD.2 SbfsStreamBlocks
CALL sbfsSetWord
SETD.0 SbfsStreamTail
LDA.0
SETD.2 SbfsFileTail
STA.2
SETD.0 SbfsStreamTo
LDD.1.0
CALL sbfsReadOne
RET
; SbfsFileBlocks and SbfsFileTail say how big it turned out to be, which need not be how
; big it was started at.
;
; A WRITER MAY ASK FOR MORE ROOM THAN IT ENDS UP USING, and that is not laziness on its
; part. Some sizes are not knowable until the last byte is out: the assembler cannot say
; how many vectors a program installs until it has resolved them, and by then the file it
; is writing into must already exist. So the room is taken generously at the start, where
; running out costs nothing, and the size is told the truth here.
;
; The blocks that were asked for and not used go back to the free count. It is a note
; rather than the authority - the directory is - but a note worth keeping right, and two
; disks holding the same files must hold the same bytes.
sbfsStreamDone:
SETD.0 SbfsStreamOpen
LDA.0
BRA sbfsStreamNo
; What it really came to, put aside before anything walks the disk.
SETD.0 SbfsStreamNewBlocks
SETD.2 SbfsFileBlocks
CALL sbfsSetWord
SETD.0 SbfsFileTail
LDA.0
SETD.1 SbfsStreamNewTail
STA.1
; Now, and not before, the old one goes. It may not be there at all, which is what
; writing something for the first time looks like from here.
CALL sbfsStreamWhere
@@ -2433,15 +2482,67 @@ sbfsStreamDone:
CALL sbfsWipeFound
sbfsStreamNoOld:
; And the temporary takes its name, which is the whole of what committing is.
; And the temporary takes its name and its true size, which together are the whole of
; what committing is.
CALL sbfsStreamTemp
CALL sbfsScanFor
BNQ sbfsStreamNo
; How much room it was given, before the entry that says so is changed.
CALL sbfsFileExtent
SETD.0 SbfsStreamSpare
SETD.2 SbfsWantBlocks
CALL sbfsSetWord
PSHD.3
POPD.1
DPUP.1 0d06
SETD.0 SbfsStreamLeaf
CALL sbfsCopyName
PSHD.3
POPD.1
DPUP.1 0d03
SETD.0 SbfsStreamNewBlocks
CALL sbfsCopyWord
PSHD.3
POPD.1
DPUP.1 0d05
SETD.0 SbfsStreamNewTail
LDA.0
STA.1
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
BNQ sbfsStreamNo
; And the difference goes back, which is what it was given less what it kept.
SETD.0 SbfsFileBlocks
SETD.2 SbfsStreamNewBlocks
CALL sbfsSetWord
SETD.0 SbfsStreamNewTail
LDA.0
SETD.1 SbfsFileTail
STA.1
CALL sbfsFileExtent
SETD.0 SbfsStreamSpare
SETD.2 SbfsWantBlocks
CALL sbfsSubWord
RSTA
SETD.0 SbfsBlock
STA.0
INCD.0
STA.0
CALL sbfsReadBlock
BNQ sbfsStreamNo
SETD.1 SbfsBuffer
CALL sbfsBufferOut
SETD.0 SbfsBuffer
DPUP.0 0d12
SETD.2 SbfsStreamSpare
CALL sbfsAddWord
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
@@ -2771,6 +2872,14 @@ SbfsStreamPath:
0x00 0x00
SbfsStreamFrom:
0x00 0x00
SbfsStreamTo:
0x00 0x00
SbfsStreamNewBlocks:
0x00 0x00
SbfsStreamNewTail:
0x00
SbfsStreamSpare:
0x00 0x00
SbfsStreamAt:
0x00 0x00
SbfsStreamParent:
+7 -1
View File
@@ -102,7 +102,13 @@
; registers and cannot write more than 65,535.
osFileStart 0d29 ; DP0 names it, DP3 is whole blocks, A is bytes in the tail.
osFileWrite 0d30 ; DP1 is the block, A and B together are which one, from zero.
osFileDone 0d31 ; No arguments. The temporary takes the name.
osFileDone 0d31 ; DP3 is whole blocks and A the tail: how big it turned out to be.
osFileFetch 0d32 ; DP1 is where it goes, A and B are which block. Reads one back.
; osFileFetch is what lets a program keep only ONE block of a file in hand while writing
; it. Anything producing two parts of a file at once - an assembler, whose source says
; #Program and #Data in whatever order it likes - has to be able to put a block down, go
; and write somewhere else, and pick it up again where it left off.
; Q is zero if it read, DP3 is how many of its bytes are the file's:
; a whole 0d256 except in a last block that is short. That count is
; why DP3 answers and not a register - 0d256 does not fit in a byte,