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
235 lines
3.6 KiB
NASM
235 lines
3.6 KiB
NASM
; 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
|
|
"
|