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:
co-authored by
Claude Opus 5
parent
7cd5e34347
commit
9f7dffdeca
@@ -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
|
||||
"
|
||||
@@ -2050,8 +2050,12 @@ DropWalk:
|
||||
; How big an output file this can build. Everything the assembler makes has to fit here at once,
|
||||
; because a file is written in one call and there is nowhere to put half of one. CosmOS
|
||||
; itself comes to 9,564 bytes.
|
||||
; Eighteen kilobytes, and IT HAS TO AGREE WITH THE SCRATCH MAP, which says where that room
|
||||
; actually is. Three numbers in three files describe these buffers - this one, LabLimit and
|
||||
; LabRoom in labels.asm, and the map itself - and each of the three has now been the one
|
||||
; that was left behind while the other two moved.
|
||||
ImgRoom:
|
||||
0x34 0x00
|
||||
0x48 0x00
|
||||
|
||||
MagicSPBT:
|
||||
"SPBT"
|
||||
|
||||
@@ -326,6 +326,7 @@ from every assembly file in it. Several are old programs written for the bare ma
|
||||
| Edit | A line editor. |
|
||||
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
||||
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
|
||||
| Pour | Writes a file a block at a time, never holding more than one block of it. Each block is filled with a byte naming itself, so a block written to the wrong place shows up as content rather than as a length. |
|
||||
| Wander | Goes to the directory it is given and reads a file there by a bare name. The only thing that moves the machine from inside a program, and so the only thing that can check the shell puts the working directory back afterwards. |
|
||||
| More | A forward-only pager. Space advances a screen, Return one line, and q stops. |
|
||||
|
||||
@@ -509,6 +510,9 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
|
||||
| osFileInfo | DP0 names a file. Q is zero if it is there, and DP3 comes back holding how many blocks it occupies. |
|
||||
| osFileBlock | DP0 names a file, DP1 says where to put a block of it, A and B together are which block counting from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes belong to the file. |
|
||||
| osChangeDir | DP0 names a directory. Q is zero if the machine is now in it. What a program changes here, the shell puts back when the program stops. |
|
||||
| osFileStart | DP0 names a file, DP3 is how many whole blocks and A is the bytes left over in the last one. Q is zero if a write is now open. Nothing already on the disk is touched. |
|
||||
| osFileWrite | DP1 is a block, A and B together are which block of the file it is, counting from zero. Q is zero if it was written. An index past the end of the file is refused. |
|
||||
| osFileDone | No arguments. The old file goes and what was written takes its name. Q is zero if it was committed. |
|
||||
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
|
||||
| osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. |
|
||||
|
||||
@@ -641,6 +645,46 @@ Finding a file and listing what is there are different jobs. sbfsFind searches f
|
||||
|
||||
A file's size is settled when it is made, because nothing can grow one afterwards. Files are laid down contiguously, so the block after a file usually belongs to somebody else. A program that does not know how much it will write has to guess high and accept the slack, or build its output elsewhere and make the file once the size is known.
|
||||
|
||||
### Writing A File Too Big To Hold:
|
||||
|
||||
`osFileSave` is handed a whole document at once, which is what a text editor has. A program
|
||||
that produces its output a piece at a time - an assembler, say - would have to hold all of
|
||||
it first, and the largest thing on this machine would then be limited by memory rather than
|
||||
by the disk.
|
||||
|
||||
So there is the other half of the streaming pair. `osFileInfo` and `osFileBlock` read a
|
||||
file a block at a time; `osFileStart`, `osFileWrite` and `osFileDone` write one.
|
||||
|
||||
```text
|
||||
SETD.0 Name
|
||||
SETD.3 0x00 0x06 ; six whole blocks
|
||||
INIA 0d40 ; and forty bytes after them
|
||||
SWI osFileStart
|
||||
|
||||
...for each block: DP1 the bytes, A and B which block...
|
||||
SWI osFileWrite
|
||||
|
||||
SWI osFileDone
|
||||
```
|
||||
|
||||
**One write is open at a time, and the system holds it rather than the program.** 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`.** 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. That is stronger than `osFileSave` can manage, where the size is only known once the
|
||||
caller already has every byte in hand.
|
||||
|
||||
Two limits differ between the two. `osFileSave` is handed a byte count in two registers and
|
||||
so cannot write more than 65,535 bytes; `osFileStart` is told blocks and a tail, the way an
|
||||
entry holds a size, and reaches the whole disk. And `osFileWrite` refuses an index past the
|
||||
end of the file - 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.
|
||||
|
||||
### Saving Something Twice:
|
||||
|
||||
Which is why saving a document is not the same as writing a file, and why sbfsSaveFile exists rather than each tool doing it. A file that has grown will usually not fit where it was, so saving it means putting it somewhere else and letting go of where it was - and **the obvious order is a trap**:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -81,6 +81,28 @@
|
||||
; relative to here, so a program given a directory to work in can say "notes.txt" and mean
|
||||
; the one in it.
|
||||
osChangeDir 0d28
|
||||
|
||||
; ---- Writing a file a block at a time ----
|
||||
;
|
||||
; The mirror of osFileInfo and osFileBlock, and the way to write something too big to hold
|
||||
; in memory. osFileSave stays for a whole document handed over at once, which is what a
|
||||
; text editor has and what most programs want.
|
||||
;
|
||||
; ONE WRITE IS OPEN AT A TIME AND THE SYSTEM 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 must 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 that already exists is touched until osFileDone, so a disk without room says so
|
||||
; while the old file is still there.
|
||||
;
|
||||
; osFileStart is told the size the way an entry holds one, blocks and a tail, rather than a
|
||||
; count of bytes - so it reaches the whole disk. osFileSave is handed a byte count in two
|
||||
; 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.
|
||||
; 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,
|
||||
|
||||
@@ -110,6 +110,38 @@ else
|
||||
report FAIL "a file down a path" "$(cmp host.img machine.img 2>&1 | head -1)"
|
||||
fi
|
||||
|
||||
# ---- A file written a block at a time ----
|
||||
#
|
||||
# The machine never holds more than 256 bytes of this file, and the host reads it back
|
||||
# whole. Every block is filled with a byte that says which block it is, so a block written
|
||||
# into the wrong place of the file is visible rather than merely being the right length -
|
||||
# which is the failure this actually had while it was being built, and the one a check on
|
||||
# the size alone would have passed.
|
||||
#
|
||||
# The last block is a part block on purpose. A tail is where every off-by-one in a
|
||||
# filesystem hides.
|
||||
"$ASM" -I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Pour.asm" -o Pour.sbx >/dev/null 2>&1
|
||||
"$TOOL" format poured.img 256 2 >/dev/null
|
||||
"$TOOL" put poured.img Pour.sbx >/dev/null
|
||||
printf 'Pour 12\nexit\n' | "$EMU" cosmos.bin --fast --disk poured.img >/dev/null 2>&1
|
||||
"$TOOL" get poured.img poured.dat gotPoured.dat >/dev/null 2>&1
|
||||
|
||||
if [ -f gotPoured.dat ] && [ "$(wc -c < gotPoured.dat | tr -d ' ')" = "3112" ]; then
|
||||
report ok "streamed out block by block" "3112 bytes, twelve blocks and a tail"
|
||||
else
|
||||
report FAIL "streamed out block by block" "wanted 3112 bytes, got $(wc -c < gotPoured.dat 2>/dev/null || echo nothing)"
|
||||
fi
|
||||
|
||||
# Every byte has to name the block it came from, or a block went somewhere else.
|
||||
if od -An -v -tu1 gotPoured.dat 2>/dev/null | awk '
|
||||
{ for (i = 1; i <= NF; i++) { if ($i != 65 + int(n / 256)) bad = 1; n++ } }
|
||||
END { exit (bad ? 1 : 0) }'; then
|
||||
report ok "and every block landed" "each byte names its own block"
|
||||
else
|
||||
report FAIL "and every block landed" "a block is not where it was written"
|
||||
fi
|
||||
|
||||
# ---- And each can read what the other wrote ----
|
||||
#
|
||||
# Matching bytes and being readable are not the same claim. A field both of them write
|
||||
|
||||
@@ -3,11 +3,11 @@ CosmOS
|
||||
> two stops, and what the registers were at each
|
||||
break at 400E
|
||||
A 11 B 22 Q 00 status 00
|
||||
DP0 2030 DP1 09C4 DP2 0000 DP3 4000 SP FFFF
|
||||
DP0 2030 DP1 09F0 DP2 0000 DP3 4000 SP FFFF
|
||||
press a key
|
||||
break at 4023
|
||||
A 44 B 55 Q 00 status 00
|
||||
DP0 2000 DP1 09C4 DP2 0000 DP3 4000 SP FFF5
|
||||
DP0 2000 DP1 09F0 DP2 0000 DP3 4000 SP FFF5
|
||||
press a key
|
||||
carried on to the end
|
||||
finished
|
||||
|
||||
@@ -416,6 +416,7 @@ app-Files | CosmOS/Apps/Files.asm | assemble | -
|
||||
app-Stream | CosmOS/Apps/Stream.asm | assemble | - | -
|
||||
app-Type | CosmOS/Apps/Type.asm | assemble | - | -
|
||||
app-Wander | CosmOS/Apps/Wander.asm | assemble | - | -
|
||||
app-Pour | CosmOS/Apps/Pour.asm | assemble | - | -
|
||||
app-More | CosmOS/Apps/More.asm | assemble | - | -
|
||||
# The assembler that runs on the machine, and its parts. Checked on their own so that a
|
||||
# failure reads as "it does not assemble" rather than as a broken disk image.
|
||||
|
||||
Reference in New Issue
Block a user