Every file service took a name a program already knew - read it, save it, rename it, delete it, ask how big it is - and none of them could find out what names there are. dir could list only because it lives in the shell and calls the filesystem directly. So a file manager, a backup, and the package manager still to come were each unwritable for want of this. osDirFirst and osDirNext. DP0 says where to put the name and B how much room, the same bargain osArgument and osWhereAmI offer. Q ANSWERS THE KIND rather than a yes or no, so one value says both whether there is an entry and what it is: 0 a file, 1 a directory, 2 a save that stopped before it committed, 0xFF nothing more. A caller that only wants names tests for 0xFF and ignores the rest. The size is deliberately not in it. A walk hands back a name, and a program that wants the size asks osFileInfo about that name - the alternative being a record in memory whose shape both sides have to agree on, which services.asm went out of its way to avoid for file sizes. Walk.asm is the first program that can see a directory, and it asks osFileInfo about each entry BETWEEN two steps of the walk. That is the hazard rather than decoration: where a walk has got to and where the last file asked about lives are both held by the system, and two things sharing one position would show as a listing that stopped early or said a name twice. Then the same walk in /Apps, since one that only ever ran at the root would not have proved it walks where you are. A directory is not asked about at all. osFileInfo answers for one perfectly well and says nought blocks, which is true and reads as a size - and nought is a size a file can genuinely have. The lint baseline moves by one. The kind is decided by a chain of bit tests in the shape dir already uses five hundred lines away, and arms of a comparison chain each loading the same variable are the case this project's own rule says not to collapse: the repetition is what lets a new arm be dropped in anywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
245 lines
13 KiB
NASM
245 lines
13 KiB
NASM
; The services the system offers, named and numbered.
|
|
;
|
|
; Both sides include this. The system follows it with handlers for the ones it implements.
|
|
; A program that only calls them includes this and nothing else, and can then say them by
|
|
; name, because a line with a name and nothing after it declares what a vector is called
|
|
; and what number it has without claiming to implement it.
|
|
;
|
|
; THE NUMBERS ARE WRITTEN DOWN HERE, and that is the only place they are written. They
|
|
; used to be decided by the order of the lines, which worked and was quietly fragile: a
|
|
; service inserted in the middle renumbered everything after it, and a program already
|
|
; assembled against the old numbers would go on calling the number rather than the name.
|
|
; Worse, the numbers a program got for its OWN traps moved depending on whether it had
|
|
; included this file, and a program that had not was given 16 - which is osPrintString.
|
|
;
|
|
; So these are pinned. They come from the range set aside for numbers that two separately
|
|
; assembled programs have to agree about; everything a program names for itself is drawn
|
|
; from higher up and cannot collide with these however it is built. Adding a service takes
|
|
; the next free number here and disturbs nothing.
|
|
;
|
|
; Written by Anachronaut
|
|
|
|
#Vectors
|
|
|
|
osPrintString 0d16 ; DP0 names a string. Prints it.
|
|
osReadLine 0d17 ; DP0 names somewhere to put a line read from the console.
|
|
osExit 0d18 ; Give the machine back to the system.
|
|
osArgument 0d19 ; DP0 names somewhere to put the rest of the run command.
|
|
|
|
; ---- What the system does with the disk on a program's behalf ----
|
|
;
|
|
; A loaded program that wanted a file used to include the whole filesystem, which is two
|
|
; and a half kilobytes of it carrying a private copy of code the system already has
|
|
; running. These are that code, reachable.
|
|
;
|
|
; NOTHING HERE MOUNTS ANYTHING. The system mounted the disk before it read the prompt, and
|
|
; there is one disk with one buffer registered as one bank; a program mounting it again was
|
|
; only ever an artefact of having its own copy of the library.
|
|
;
|
|
; Sizes are in bytes and fit the registers exactly. A file that can be read into Data
|
|
; Memory is under 64K by definition, so its length is sixteen bits: coming back it is DP3,
|
|
; going out it is A and B together, and neither direction needs a record in memory that
|
|
; both sides have to agree on the shape of.
|
|
osFileRead 0d20 ; DP0 names it, DP1 says where. Q is zero if it read, DP3 is how many bytes.
|
|
; ---- AND IT WRITES WHOLE BLOCKS ----
|
|
;
|
|
; A disk is read a block at a time, so a sixteen byte file still puts
|
|
; 256 bytes where it is told to. The room given has to be the file's
|
|
; length ROUNDED UP to the next 256, and a caller that gives exactly
|
|
; the length writes over whatever follows it. DP3 still says how many
|
|
; bytes are the file's; the rest is whatever was on the block.
|
|
osFileSave 0d21 ; DP0 names it, DP1 is the bytes, A and B are how many. Q is zero if it saved.
|
|
osFileDelete 0d22 ; DP0 names it. Q is zero if it went.
|
|
osFileRename 0d23 ; DP0 is the name it has, DP1 the name it should have. Q is zero if it moved.
|
|
|
|
; ---- Reading a file that will not fit ----
|
|
;
|
|
; osFileRead answers with a whole file in Data Memory, which settles it for anything under
|
|
; 64K and settles nothing above. These two are the other way of asking: how big is it, and
|
|
; then give me one block of it at a time. Nothing is kept between the calls but the number
|
|
; of the block wanted, so there is no handle to open, none to close, and nothing left
|
|
; behind by a program that stops in the middle. The system remembers where the last file it
|
|
; was asked about lives, so asking for four hundred blocks of one file costs one search of
|
|
; the directory rather than four hundred; that is a speed, not a promise, and a caller
|
|
; never has to know about it.
|
|
;
|
|
; THESE TWO SAY WHY WHEN THE ANSWER IS NO, which the others do not. Everywhere else the
|
|
; only useful thing to do about a failure is to give up, so one value is enough. These
|
|
; exist to be asked questions with - is it there, is there any more of it - and the
|
|
; difference between "no disk", "no such file" and "that was the last block" is the answer
|
|
; rather than an excuse.
|
|
;
|
|
; 1 there is no disk
|
|
; 2 there is no file of that name
|
|
; 3 that block is past the end of the file (osFileBlock only)
|
|
; 4 the disk would not read it (osFileBlock only)
|
|
osFileInfo 0d26 ; DP0 names it. Q is zero if it is there, DP3 is how many blocks.
|
|
osFileBlock 0d27 ; DP0 names it, DP1 says where, A and B are which block from zero.
|
|
|
|
; ---- Moving about ----
|
|
;
|
|
; 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 - the same
|
|
; discipline the Stack and the vector table are held to, and for the same reason. A program
|
|
; is entitled to move about; the shell is entitled to find itself where it left off.
|
|
;
|
|
; This is what makes a bare name mean something to a program: everything a program opens is
|
|
; 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 ; DP3 is whole blocks and A the tail: how big it turned out to be.
|
|
osFileFetch 0d32 ; DP1 is where it goes, A and B are which block. Reads one back.
|
|
|
|
; osFileFetch is what lets a program keep only ONE block of a file in hand while writing
|
|
; it. Anything producing two parts of a file at once - an assembler, whose source says
|
|
; #Program and #Data in whatever order it likes - has to be able to put a block down, go
|
|
; and write somewhere else, and pick it up again where it left off.
|
|
; 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,
|
|
; and a count that lied about a full block would make every reader
|
|
; treat the end of a file as a special case.
|
|
|
|
; ---- And with the console ----
|
|
;
|
|
; printString is already up there. This is the other half of what a program prints: a
|
|
; number, in decimal, without leading zeroes. A and B together, so one service covers both
|
|
; a line number and a byte count and there is no need for two.
|
|
osPrintNumber 0d24
|
|
|
|
; ---- Stopping to look ----
|
|
;
|
|
; A breakpoint. Put SWI osBreak anywhere in a program and the system shows every register as
|
|
; the program had them, waits for a key, and carries on.
|
|
;
|
|
; NOTHING IS OVERWRITTEN, which is what makes this simple. A breakpoint that replaced an
|
|
; instruction would have to put it back to continue, and putting it back disarms the
|
|
; breakpoint - so firing twice would need the instruction to be stepped over and the
|
|
; breakpoint replaced behind it, and this machine has no way to step one instruction. An SWI
|
|
; costs two bytes of the program and fires for ever, because there was never anything to
|
|
; restore. The price is that it is part of the program: a build with breakpoints in it has
|
|
; different addresses from one without.
|
|
osBreak 0d25
|
|
|
|
; ---- How the last start went ----
|
|
;
|
|
; The loader marks the disk before it hands over and the system clears the mark on reaching
|
|
; its prompt, so a mark still set is a start that never arrived. See the boot state in
|
|
; sbfs.h for what the numbers mean.
|
|
;
|
|
; osBootState answers in Q: 0 settled, 1 trying, 2 fell back. A machine with no disk answers
|
|
; settled, because there is nothing there to be unsettled about.
|
|
;
|
|
; osBootSettle puts it back to settled, which is how a machine that fell back is told the
|
|
; situation has changed. Q is zero if the disk took it.
|
|
;
|
|
; THE ONLY WRITE A PROGRAM GETS IS SETTLING. Marking a start as trying or fallen back is
|
|
; the loader's business, and a service that let a program claim either would let it lie
|
|
; about something the loader has no way to check.
|
|
osBootState 0d33
|
|
osBootSettle 0d34
|
|
|
|
; ---- What a program made of it ----
|
|
;
|
|
; osExit takes a status in A: zero if the program did what it was asked, and a number of
|
|
; its own choosing if it did not. IN A RATHER THAN Q, which is not a departure from the
|
|
; rule that a service answers in Q - this one takes an argument, the way osPrintNumber
|
|
; does, and never returns to answer anything. A is free precisely because a return would
|
|
; have put it back, and Q is the ALU's output, so a small number costs four instructions
|
|
; there and one in A.
|
|
;
|
|
; osLastStatus answers in Q with what the last program exited with. The shell does not
|
|
; print it: a program that failed has already said so in words, and a number beside that
|
|
; would be noise. This is for the thing that cannot read words.
|
|
osLastStatus 0d35
|
|
|
|
; ---- Taking the screen, and giving it back ----
|
|
;
|
|
; Says that this program is about to use the whole screen and would like what is on it now put
|
|
; back when it exits. Q is zero if that was arranged; anything else means it was not, which is
|
|
; the ordinary answer on a machine with no volatile drive to keep it on - and a program told
|
|
; no should carry on regardless, because it was going to before this existed.
|
|
;
|
|
; NOT AUTOMATIC, and that is the point: dir and Say print and stop, and their output is the
|
|
; reason you ran them. Only a program that says it took the screen gets the screen put back.
|
|
osTakeScreen 0d36
|
|
|
|
; ---- Where the program itself came from ----
|
|
;
|
|
; DP0 says where to put it and B is how much room there is counting the zero, the same
|
|
; bargain osArgument offers. What comes back is the whole path the program was loaded from,
|
|
; made absolute: a program found where somebody was standing is named by the bare word that
|
|
; was typed, and a bare name means the working directory - which a program is entitled to
|
|
; move out of.
|
|
;
|
|
; ---- Why this and not "open a file beside me" ----
|
|
;
|
|
; A service that opened a file relative to the program would need a TWIN FOR EVERY FILE
|
|
; OPERATION there is: read, save, info, block, start, write, done, delete, rename. One
|
|
; service handing back a path composes with all of them, and joining a name to a place is
|
|
; then a library rather than a service - Libraries/path.asm, beside print.asm and math.asm.
|
|
;
|
|
; ---- And why not just move the program there ----
|
|
;
|
|
; Because a program's ASSETS are relative to the program and its ARGUMENTS are relative to
|
|
; the person, and the working directory can only be one of them. Setting it to the program's
|
|
; own would mean "Play mytune.tune", typed by somebody in their own directory, looked in
|
|
; Play's. The working directory stays the person's; this is how a program finds its own.
|
|
osWhereAmI 0d37
|
|
|
|
; ---- Seeing what is on the disk ----
|
|
;
|
|
; Everything above takes a name a program already knows. NOTHING HERE COULD FIND OUT WHAT
|
|
; NAMES THERE ARE - a machine whose programs can read, write, rename and delete files and
|
|
; cannot ask what files exist. dir could only list because it lives in the shell and calls
|
|
; the filesystem directly; no loaded program could list anything at all.
|
|
;
|
|
; DP0 says where to put the name and B how much room there is, counting the zero, the same
|
|
; bargain osArgument and osWhereAmI offer.
|
|
;
|
|
; Q ANSWERS THE KIND RATHER THAN A YES OR NO, which is one value carrying both "is there
|
|
; one" and "what is it":
|
|
;
|
|
; 0 a file
|
|
; 1 a directory
|
|
; 2 a save that stopped before it committed
|
|
; 0xFF there are no more
|
|
;
|
|
; ---- And the size is not in it ----
|
|
;
|
|
; A walk hands back a NAME, and a program that wants the size of what it found asks
|
|
; osFileInfo about that name. The alternative is a record in memory whose shape both sides
|
|
; have to agree on, which is exactly what this file went out of its way to avoid for file
|
|
; sizes - and most callers of this want names and nothing else.
|
|
;
|
|
; ---- One walk at a time, and the system holds it ----
|
|
;
|
|
; Where a walk has got to is the system's, the way an open write is. A program that starts a
|
|
; second walk before finishing the first gets the second; there is one position, not a handle
|
|
; per caller. That is the same bargain osFileStart makes and for the same reason: the state
|
|
; is small, and a program that stops in the middle leaves nothing behind to clean up.
|
|
osDirFirst 0d38
|
|
osDirNext 0d39
|