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
+234
View File
@@ -0,0 +1,234 @@
; Writes a file one block at a time, without ever holding the whole of it.
;
; This is the write side of what Stream demonstrates for reading: a file bigger than the
; memory building it. It writes as many blocks as it is asked for, each one filled with a
; pattern that says which block it is, so that what comes off the disk afterwards can be
; checked against what should have gone on rather than merely being the right length.
;
; The argument is how many blocks, in decimal. The last one is deliberately a part block,
; because a tail is the case every off-by-one in a filesystem hides in.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Argument
INIB 0d8
SWI osArgument
SETD.0 Argument
LDA.0
BRA useDefault
CALL readCount
BRI counted
useDefault:
INIA 0d4
SETD.0 Blocks
STA.0
counted:
; Nothing may be zero blocks: the tail below would then be the whole file.
SETD.0 Blocks
LDA.0
BNA haveCount
INIA 0d1
SETD.0 Blocks
STA.0
haveCount:
; Whole blocks, and a tail of forty bytes on the end of them.
SETD.0 Name
SETD.3 0x00 0x00
SETD.0 Blocks
LDA.0
PSHA
POPB
RSTA
PSHA
PSHB
POPD.3
SETD.0 Name
INIA 0d40
SWI osFileStart
BNQ startFailed
; Each whole block, filled with its own number.
RSTA
SETD.0 Which
STA.0
nextBlock:
SETD.0 Which
LDA.0
SETD.2 Blocks
LDB.2
CCF
SUB
BRQ theTail
CALL fillBlock
SETD.1 Block
SETD.0 Which
LDA.0
RSTB
PSHA
POPB
RSTA
SWI osFileWrite
BNQ writeFailed
SETD.0 Which
LDA.0
INCA
STA.0
BRI nextBlock
theTail:
; And the part block at the end, which is the same fill cut short by the size given at
; the start. Only the first forty bytes of it will belong to the file.
CALL fillBlock
SETD.1 Block
SETD.0 Which
LDA.0
RSTB
PSHA
POPB
RSTA
SWI osFileWrite
BNQ writeFailed
SWI osFileDone
BNQ doneFailed
SETD.0 Wrote
SWI osPrintString
SETD.0 Blocks
LDB.0
RSTA ; A and B together are the number, so the count is the low half.
SWI osPrintNumber
SETD.0 AndTail
SWI osPrintString
SWI osExit
; The block becomes 256 copies of the block number plus a fixed byte, so that a block
; written into the wrong place is visible rather than merely being bytes.
fillBlock:
SETD.0 Block
SETD.1 Which
LDA.1
INIB 0x41
CCF
ADD
MVQA
RSTB
fillLoop:
STA.0
INCD.0
DECB
BNB fillLoop
RET
; The argument, in decimal, into Blocks. Anything that is not a digit ends it.
readCount:
RSTA
SETD.1 Blocks
STA.1
SETD.0 Argument
countLoop:
LDA.0
BRA countDone
INIB 0x30
CCF
SUB
MVQA
INIB 0d10
CCF
SUB
BNC countDone ; Not a digit, so the number ended.
SETD.1 Blocks
LDB.1
PSHA
RSTA
INIA 0d10
CALL timesTen
POPA
SETD.1 Scratch
LDB.1
CCF
ADD
MVQA
SETD.1 Blocks
STA.1
INCD.0
BRI countLoop
countDone:
RET
; B times ten into Scratch, by adding it up. Nothing here is bigger than a byte.
timesTen:
RSTA
SETD.0 Scratch
STA.0
INIA 0d10
tenLoop:
PSHA
SETD.0 Scratch
LDA.0
SETD.2 TenHold
STB.2
LDB.2
CCF
ADD
MVQA
SETD.0 Scratch
STA.0
POPA
DECA
BNA tenLoop
RET
startFailed:
SETD.0 NoStart
SWI osPrintString
SWI osExit
writeFailed:
SETD.0 NoWrite
SWI osPrintString
SWI osExit
doneFailed:
SETD.0 NoDone
SWI osPrintString
SWI osExit
#Data
#Base 0x2000
Name:
"poured.dat"
Argument:
#Reserve 0d9
Blocks:
0x00
Which:
0x00
Scratch:
0x00
TenHold:
0x00
Block:
#Reserve 0d256
Wrote:
"poured "
AndTail:
" blocks and a tail of 40
"
NoStart:
"could not start it
"
NoWrite:
"could not write a block
"
NoDone:
"could not finish it
"