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
+44
View File
@@ -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**: