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
+76
View File
@@ -925,6 +925,79 @@ loadRefuse:
STA.1
RET
; ---- Writing a file a block at a time ----
;
; Three handlers over the three routines in sbfs.asm, and thin, because everything that is
; difficult about writing safely is down there where it is written once.
;
; DP0 names the file, DP3 is how many whole blocks and A is what is left over in the last
; one - the size said the way an entry says it, which is what lets this reach past the
; 65,535 bytes osFileSave can describe.
handleFileStart:
SETD.2 DiskReady
PSHA
LDA.2
BRA fileStartNoDisk
POPA
; The tail is in A and the block count is in DP3, so the count has to come out of the
; pointer before anything else wants it.
SETD.2 SbfsFileTail
STA.2
; The low byte comes off the Stack first, which is the same way round Type and every
; other reader takes a count out of DP3. Popping them the other way gave a block count
; of the size times two hundred and fifty six, and a start that could find no room.
PSHD.3
POPB
POPA
SETD.2 SbfsFileBlocks
STA.2
INCD.2
STB.2
; What a name means on the disk is about to change, so the remembered file goes.
CALL fileForget
CALL sbfsStreamStart
MVQA
MVSD.2
DPUP.2 0d02
STA.2
RETI
fileStartNoDisk:
POPA
MVSD.2
DPUP.2 0d02
INIA 0d1
STA.2
RETI
; DP1 is where the block comes from, and A and B together say which block of the file it
; is, counting from zero - the same way osFileBlock is told which one to fetch.
handleFileWrite:
SETD.2 SbfsIndex
STA.2
INCD.2
STB.2
CALL sbfsStreamWrite
MVQA
MVSD.2
DPUP.2 0d02
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.
handleFileDone:
CALL fileForget
CALL sbfsStreamDone
MVQA
MVSD.2
DPUP.2 0d02
STA.2
RETI
; ---- osChangeDir ----
;
; The service behind the shell's cd, and the reason the shell bothers to put the working
@@ -3668,6 +3741,9 @@ CommandLine:
osFileInfo handleFileInfo
osFileBlock handleFileBlock
osChangeDir handleChangeDir
osFileStart handleFileStart
osFileWrite handleFileWrite
osFileDone handleFileDone
osPrintNumber handlePrintNumber
osBreak handleBreak
Device 0x20 diskDone