S1: the write side learns to stream

osFileStart, osFileWrite and osFileDone are the mirror of osFileInfo and
osFileBlock. A program can now write a file it never holds: Pour writes twelve
blocks and a tail while keeping 256 bytes of it at a time, and the host tool
reads all 3,112 bytes back with every block where it was put.

ONE WRITE IS OPEN AT A TIME AND COSMOS HOLDS IT. Reading needs no state - a name
and an index are the whole question - but writing safely does, because the new
file has to exist before the old one is thrown away and something has to
remember which temporary belongs to which name. Keeping that here means the
careful order is written once instead of in every program that streams.

Nothing already on the disk is touched until osFileDone, so a disk without room
says so while the old file is still there. That is stronger than osFileSave can
manage, where the size is only known once the caller has every byte in hand.
osFileSave stays: Edit and Files hand over whole documents and have no reason to
learn any of this.

osFileWrite refuses an index past the end of the file, and that refusal is not
politeness. Files are contiguous, so block nine of a three block file is a real
block belonging to something else, and writing it would put one file's bytes
inside another with nothing anywhere saying so. Checked both ways: the tail
block is allowed and the one past it is not.

Three bugs, all of them the same shape - a register or pointer used for two
things at once:

DP3 carried the block count in and was popped high byte first, which is the
wrong way round from every reader in the system and made the count two hundred
and fifty six times too big.

sbfsStreamStart took the name in DP0 and then wanted DP0 for something else
before it had read it, so it walked whatever it last pointed at and reported
that it could find no room.

sbfsStreamWrite kept the caller's block in DP3 across a find - DP3 being the
pointer a return does not put back, which is exactly why the find uses it too.
What went to the disk was whatever the scan last looked at. It goes in memory
now, and the file is correct because every block says which block it is; a
check on the length alone would have passed all three of these.

Writing no longer finds the file for each block either. Nothing moves a file
once it is made, so where it starts is settled when the temporary is created.
That was not even slow - a scan stops the moment it matches - but it was a walk
of the directory per block for an answer that cannot change, and it is 28 per
cent of the cost of writing forty blocks.

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 14:53:37 -04:00
co-authored by Claude Opus 5
parent 7cd5e34347
commit 9f7dffdeca
9 changed files with 673 additions and 3 deletions
+257
View File
@@ -1238,6 +1238,47 @@ sbfsReadOne:
sbfsReadOneDone:
RET
; ---- Writing one block of a file ----
;
; The other half of sbfsReadOne, and deliberately the same shape. DP1 is where the block
; comes FROM and SbfsIndex says which block of the file it is, counting from zero. The file
; is whichever one the last find landed on, exactly as it is for reading.
;
; THE INDEX IS CHECKED AGAINST THE FILE'S LENGTH, and that check is not politeness. Files
; are laid down contiguously, so block N of a five block file is a real block belonging to
; whatever happens to sit five blocks along - and writing it would put one file's bytes
; inside another with nothing anywhere saying so. Reading past the end is a wrong answer;
; writing past it is somebody else's file.
sbfsWriteOne:
PSHD.1
POPD.3 ; Where it comes from. DP3 survives the calls below.
CALL sbfsFileExtent ; How many blocks the file really occupies.
SETD.0 SbfsIndex
SETD.2 SbfsWantBlocks
CALL sbfsCompareWord
BNC sbfsWriteOneNo ; The index is not below the count, so it is past the end.
SETD.0 SbfsBlock
SETD.2 SbfsFileStart
CALL sbfsSetWord
SETD.0 SbfsBlock
SETD.2 SbfsIndex
CALL sbfsAddWord
PSHD.3
POPD.1
CALL sbfsBufferIn
CALL sbfsWriteBlock
RET ; Q is whatever the write said.
sbfsWriteOneNo:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Writing ----
; Where the first block that can hold a file is: past the superblock and the directory.
@@ -2241,6 +2282,202 @@ sbfsRenameFailed:
ADD
RET
; ---- Writing a file a block at a time ----
;
; The mirror of reading one, and it needs something reading does not: state. A read is a
; whole question in itself - here is a name and an index, hand me that block - but a safe
; write cannot be, because the new file has to exist somewhere before the old one is thrown
; away, and something has to remember which temporary belongs to which name between one
; block and the next.
;
; ONE WRITE IS OPEN AT A TIME AND COSMOS HOLDS IT, rather than the program being handed
; something to keep. The careful order below is the one sbfsSaveFile uses and it is not
; obvious; leaving it to each program that streams would mean every one of them getting it
; right separately, and the cost of getting it wrong is somebody's file.
;
; sbfsStreamStart DP0 names it, SbfsFileBlocks and SbfsFileTail say how big
; sbfsStreamWrite DP1 is the block, SbfsIndex says which one it is
; sbfsStreamDone the temporary takes the name, and the old file goes
;
; NOTHING THAT ALREADY EXISTS IS TOUCHED UNTIL THE LAST OF THOSE. The room for the whole
; file is taken at the start, so a disk that cannot hold it says so while the old one is
; still there - which is better than the order sbfsSaveFile has to use, where the size is
; only known once the caller has the bytes in hand.
sbfsStreamStart:
; WHERE THE NAME IS, KEPT FIRST OF ALL. DP0 holds it on the way in and everything below
; wants DP0 for something else, so the pointer has to be put somewhere before the first
; of those - not after, which is a routine that walks whatever it last happened to point
; at and reports that it could find no room.
SETD.2 SbfsStreamPath
STD.0.2
; The size, put aside before anything walks the disk. Finding things overwrites the place
; a size is normally said, because finding describes whatever it last looked at.
SETD.0 SbfsStreamBlocks
SETD.2 SbfsFileBlocks
CALL sbfsSetWord
SETD.0 SbfsFileTail
LDA.0
SETD.1 SbfsStreamTail
STA.1
RSTA
SETD.0 SbfsStreamOpen
STA.0 ; Not open until it is.
SETD.2 SbfsStreamPath
LDD.0.2
CALL sbfsWalkParent
BNQ sbfsStreamNo
SETD.0 SbfsAt
SETD.1 SbfsStreamParent
CALL sbfsCopyWord
SETD.0 SbfsWanted
SETD.1 SbfsStreamLeaf
CALL sbfsCopyName
; Refused if that name belongs to a directory, and refused now rather than at the end
; with a written temporary nothing would ever come back for.
CALL sbfsStreamWhere
CALL sbfsScanFor
BNQ sbfsStreamFresh
SETD.0 SbfsFoundFlags
LDA.0
INIB 0x02
AND
BNQ sbfsStreamNo
sbfsStreamFresh:
; A temporary left by a stream that did not finish would be in the way. Whether there was
; one is not worth asking about, since either answer leads here.
CALL sbfsStreamTemp
CALL sbfsScanFor
BNQ sbfsStreamNoTemp
CALL sbfsWipeFound
sbfsStreamNoTemp:
; And the room for all of it, taken while the old file is still safe.
SETD.0 SbfsFileBlocks
SETD.2 SbfsStreamBlocks
CALL sbfsSetWord
SETD.0 SbfsStreamTail
LDA.0
SETD.1 SbfsFileTail
STA.1
CALL sbfsStreamTemp
CALL sbfsCreateAt
BNQ sbfsStreamNo
; WHERE THE TEMPORARY BEGINS, KEPT NOW. Nothing moves a file once it is made, so every
; block after this one can be written without looking it up again. Finding it each time
; worked and was not even slow - a scan stops the moment it matches, and a temporary
; lands in an early slot - but it is a walk of the directory per block for an answer that
; cannot have changed, and on a disk whose early entries are all taken it would be a walk
; of the whole thing.
SETD.0 SbfsStreamAt
SETD.2 SbfsFileStart
CALL sbfsSetWord
INIA 0x01
SETD.0 SbfsStreamOpen
STA.0
RSTA
RSTB
CCF
ADD
RET
sbfsStreamNo:
RSTA
INIB 0d1
CCF
ADD
RET
; DP1 is the block and SbfsIndex says which one. The temporary is found again each time,
; because everything in between has been reading other blocks over the one it lives in.
sbfsStreamWrite:
SETD.0 SbfsStreamOpen
LDA.0
BRA sbfsStreamNo
; Where the file is and how big it is, said again rather than looked up: all three were
; settled when the temporary was made, and everything since has been describing whatever
; it last looked at.
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
CALL sbfsWriteOne
RET
sbfsStreamDone:
SETD.0 SbfsStreamOpen
LDA.0
BRA sbfsStreamNo
; 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
CALL sbfsScanFor
BNQ sbfsStreamNoOld
CALL sbfsWipeFound
sbfsStreamNoOld:
; And the temporary takes its name, which is the whole of what committing is.
CALL sbfsStreamTemp
CALL sbfsScanFor
BNQ sbfsStreamNo
PSHD.3
POPD.1
DPUP.1 0d06
SETD.0 SbfsStreamLeaf
CALL sbfsCopyName
SETD.1 SbfsBuffer
CALL sbfsBufferIn
CALL sbfsWriteBlock
BNQ sbfsStreamNo
RSTA
SETD.0 SbfsStreamOpen
STA.0
RSTA
RSTB
CCF
ADD
RET
; The directory the file is going in, and the name it will end up under. Said again before
; each step, because every step goes to the disk and leaves the walk somewhere else.
sbfsStreamWhere:
SETD.0 SbfsStreamParent
SETD.1 SbfsAt
CALL sbfsCopyWord
SETD.0 SbfsStreamLeaf
SETD.1 SbfsWanted
CALL sbfsCopyName
RET
; The same directory, under the name a half written file is kept as. Its own rather than
; the one a whole file save uses, so that neither can ever be handed the other's.
sbfsStreamTemp:
SETD.0 SbfsStreamParent
SETD.1 SbfsAt
CALL sbfsCopyWord
SETD.0 SbfsStreamName
SETD.1 SbfsWanted
CALL sbfsKeepName
RET
; ---- Saving over something that is already there ----
;
; DP0 names the file, DP1 is the data, and SbfsFileBlocks with SbfsFileTail say how big it
@@ -2527,5 +2764,25 @@ SbfsSaveLeaf:
SbfsTempName:
"sbfs.part"
; ---- What a file being written a block at a time keeps ----
SbfsStreamOpen:
0x00
SbfsStreamPath:
0x00 0x00
SbfsStreamFrom:
0x00 0x00
SbfsStreamAt:
0x00 0x00
SbfsStreamParent:
0x00 0x00
SbfsStreamLeaf:
#Reserve 0d23
SbfsStreamBlocks:
0x00 0x00
SbfsStreamTail:
0x00
SbfsStreamName:
"sbfs.out"
SbfsBuffer:
#Reserve 0d256