da91a36d92ac9e5e3ef91c255b3f5135b9eb552a
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
da91a36d92 |
D4: the machine makes directories too
mkdir and rmdir are the machine's own now, and a file goes where its path says rather than always in the root. A disk can be organised without the host tool touching it. Everything below the surface works in terms of a directory and a name rather than a path. sbfsWalkParent splits the last name off, walks the rest, and hands back the two - and the separator stays on the end of the head, which is what makes one rule cover every kind of path: "/x" leaves "/", which is the root; "x" leaves nothing, which is where the machine already is; and "A/x" leaves "A/", which is neither and needs no special case to say so. Saving works in those two as well, and had to. The careful order a save uses - make a temporary, write it, delete the original, rename the temporary - only works if the temporary is made in the SAME directory as the file, because the rename at the end changes a name and does not move anything. Renaming to a path naming a different directory is refused for that reason, rather than quietly being a lie the disk goes along with. Three things this cost, all found by running it: mkdir Apps/Deep made /Apps/Apps. The leaf was worked out into SbfsWanted and then the head was walked - and walking goes through sbfsPathNext, which puts every name it meets into SbfsWanted on the way past. The head's last name landed exactly where the leaf was. It has somewhere of its own now. rmdir took a directory with something still in it, which is the one failure the whole design is arranged to prevent. Looking for children clobbered DP2 and rebuilt it from the buffer and the entry count with the subtraction the wrong way round, so the pointer walked off the end of the block and found nothing. The comparison goes through a CALL now, like the two beside it, and DP2 comes back on the entry because a RET puts it there. SplitDisk's "in use but not reachable from the root" line is what caught it. Refusing a name longer than twenty two used to read the twenty third character of a shorter one, which is somebody else's string. It is measured now. Tests/agree.sh is new and is the gate this rung was for: the same disk built twice, once with SplitDisk and once with CosmOS, compared byte for byte. The two share no code and only a written specification, and every field one writes and the other only reads is checked there and nowhere else - which entry a thing lands in, which block, what a directory's unused fields hold, the version, the free count. It caught a wrong parent immediately when that was broken on purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
36ce9f6ccf |
D3: the machine knows where it is
cd moves it, dir lists the directory it is in, and the prompt says which one - but only when that is not the root, so a machine nobody has moved about on looks exactly as it always did and every recorded test that never says "cd" keeps its recorded prompt. A path beginning with a separator is measured from the root and anything else from where the machine is, so a bare name means a file in the current directory. NO PROGRAM HAD TO BE TOLD: the working directory lives in sbfs.asm beside the thing that resolves paths, because it is what a relative path MEANS. Keeping it in the shell would have meant either handing it down on every call or pasting it onto the front of every name, and the second of those is how a name that is already absolute gets ruined. Nothing stores the path. The working directory is an entry index and two bytes, and the text on the prompt is built each time by walking the chain of parents upward, writing names from the end of a buffer towards the front - which is the order they arrive in, and saves reversing them afterwards. sbfsFind splits into a walk and a check. "cd /" and "cd .." both end at the root quite legitimately, and had no way to say so through a routine whose only word for the root was "missing". Typing a program's name now tries two places in order: where you are, then /Apps. The first makes a program you are working on the one that runs; the second lets Snake work from anywhere. A word already beginning with a separator has said where to look, so only that place is tried. osChangeDir exists so that "a program may move about, and the shell puts the working directory back" is a thing that can happen rather than a promise about nothing. Both halves of that were unfalsifiable without it: with no way for a program to move, removing the restore changed no test. Wander is the program that moves - it goes where it is told and reads a file there by a bare name - and with it on the disk, removing the restore fails. The remembered file is dropped whenever what a relative path means changes: a cd, a program calling osChangeDir, a program exiting. Removing all of them fails the test and removing any one of them does not, because today every path into that cache belongs to a program that exits. It is kept in all three because the cost is a call and the failure is a file's blocks being handed out under another file's name. The cwd fixture holds two files called notes.txt saying different things, and a Say.sbx in /A that is really hello. Two copies of one program, or two copies of one file, would have passed with the whole of this deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
588e02aff5 |
Double CosmOS's half of the machine, and check that it fits
The memory map gave CosmOS 0x0000 through 0x1FFF of Program Memory and applications 0x2000 and above. CosmOS is 8141 bytes at the previous commit, which is fifty one bytes short of the line, and the next thing added to it went over. GOING OVER DOES NOT FAIL WHERE IT HAPPENS. Nothing enforces the division: an application says where it goes with #Base and the loader puts it there, so a CosmOS that has grown past 0x1FFF simply has the next program loaded written over the end of it. What breaks is whichever part of the shell that program happened to cover, at whatever later moment somebody uses it. It turned up here as the monitor's assemble command answering "I do not know" to valid instructions, several commands into a session, on a machine that had booted perfectly well. Both halves are doubled: applications now start at 0x4000 in Program Memory and 0x2000 in Data Memory. That is 16K of code and 8K of data for the system, against the 8775 and 2948 it uses today. Both were on the same trajectory, and moving them together means the twenty files that say #Base are edited once rather than twice. The standalone loader's loadable.asm keeps its old base: it belongs to the loader CosmOS grew out of, not to CosmOS, and its addresses answer to a different program. The unbased-segment diagnostic keeps its old base too - it exists to produce an error message that names the address, and the message is what is recorded. Tests/docs.sh now reads the two limits out of the table in the README and measures both segments against them. It reads them rather than being told them because the table is the specification, and this is the second time in this project that the thing nobody checked is the thing that rotted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
36a1b07b5b |
D2: the machine walks a path
sbfsFind takes a path where it used to take a name: names with '/' between them, walked from the root, with '.' and '..'. Each name is looked for among the entries whose parent is where the walk has got to. A bare name is a path of one name, so everything written before directories existed still works and still costs one walk of the directory. sbfsMount takes either version. On a version one disk every entry has zeroes where a parent goes and the walk starts at zero, so the comparison always agrees - which is how a flat disk reads correctly here with nothing done to it. PROGRAMS DID NOT HAVE TO BE TAUGHT ANY OF THIS. Resolution sits inside sbfsFind, below the services, so every osFile call keeps its signature and a path is simply a longer name. Type, More, Edit and the assembler gained subdirectories without a line changing in any of them. Four things this turned up, none of which was the path walk: load copied the path into a buffer sized for a NAME, so anything over 22 characters was cut short - and cut short into a path that often still resolved. "/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/" and reported that the program was a directory. That is the whole of what looked like a bug in '..', and it cost most of the time here. load on a directory SUCCEEDED. A directory has no blocks, so reading it reads nothing and leaves the staging area holding whatever was staged last - which, if that was a program, still says SBEX and still has a working entry address. It handed back the program before it. Refused outright now. delete and rename on a directory are refused, and save refuses one up front rather than failing at the rename and leaving a temporary behind. Deleting a directory frees an entry index, and a parent IS an index, so the next file created would take it and inherit the children. create writes the parent rather than leaving it zero by luck. It would be zero - delete wipes all thirty two bytes and a fresh entry never had any - but that is a fact about two other routines, and a file appearing inside a directory it was never put in is not a failure anybody would think to look for. dir marks directories and counts them apart from files, because at this point it was calling them files of no bytes. Two hazards written down in the design note turned out not to be real, and both were checked rather than argued about: The lookup cache holding 22 bytes of a longer path cannot hand back the wrong file - textSame wants both strings to end in the same place, so a cut down entry misses. It can never HIT either, though, so every path longer than a name went to the disk every time; it holds a whole path now. The allocator stepping over directories changes nothing any test can see. A directory has no start as well as no blocks, so its bounds are nought to nought and no candidate begins before it ends. The four instructions stay, with a comment saying they are not load bearing today and why they are there anyway. makedisks.sh resolves its build path before it cds. Given a relative one it carried on and quietly built disks missing some of their files, which is how the tree fixture lost a file and sent me looking for a bug in '..'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2b0aeeefd4 |
Start a program by typing its name
A word the shell has no command for is now looked for on the disk as "<name>.sbx", and if it is there it is loaded and started exactly as load and run would do it. Whatever followed the word reaches the program through osArgument by the same route as whatever follows run, so "Say hello there" and "Type notes.txt" work without either program knowing how it was started. load and run are unchanged and both stay. load is how the monitor puts an arbitrary file in front of itself, which typing a name deliberately cannot do: the extension is added rather than assumed, so "notes.txt" looks for notes.txt.sbx and a text file is unreachable by name whatever is inside it. Three things this had to get right: The built-ins are tried first and always win. The search hangs off the end of the dispatch chain, so a file called dir.sbx cannot become dir, and the commands worth trusting when the disk is what you are doubting stay trustworthy. The invoke disk carries a working dir.sbx so that this is checked rather than asserted. A file that is found but is broken says so. "not a program" and "I do not know" are different answers, and giving the second about a file sitting on the disk would send somebody looking in the wrong place. loadProgram therefore hands back a status as well as a message, since only "no file of that name" can fairly be reported as anything other than a fault. doLoad became that subroutine rather than being copied. It ends in RET instead of a jump to the prompt, and each way of failing sets its number and its text together so a new one cannot leave half of the answer behind. cosmosBreak moves because Break prints the pointers it was handed and those are the shell's leftovers, which a CALL now puts back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3d2ab34229 |
Streaming: read a file bigger than the machine's memory
osFileRead hands over a whole file, which settles anything under 64K and settles nothing above it. CosmOS's own source is above it - the sources together are 104K against 64K of Data Memory - so a machine that is going to assemble itself needs another way to ask. osFileInfo (0d26) says how many blocks a file occupies. osFileBlock (0d27) hands over one of them and says how many of its bytes belong to the file. Between them a program reads a file of any size through a buffer of 256. Blocks rather than bytes from osFileInfo is forced, not chosen: a file on a sixteen megabyte disk is up to twenty four bits long and a pointer holds sixteen. osFileBlock's count answers in DP3 for the same kind of reason - a whole block is 256 bytes, which does not fit in a register, and a count that reported it as zero would make every reader special-case the end. Nothing is kept open. Every call names the file, so there is no handle to leak and nothing left behind by a program that stops halfway. Taken at its word that means searching the directory once per block, so the system remembers where the last file it was asked about lives; every path that can change what a name means calls fileForget, including the shell's own delete and rename, which do not go through the services. Correctness never depends on the cache - a cache thrown away is indistinguishable from one never filled. Measured on a 329 block file: 7% of the run saved when the file is the first directory entry, 11% when it is the sixteenth. These two say WHY when the answer is no, which the others do not. Elsewhere the only useful response to a failure is to give up, so one value suffices. These are asked questions, and running off the end is how a reader learns it has finished, so it gets an answer of its own: 1 no disk, 2 no such file, 3 past the end, 4 the disk refused. Apps/Stream.asm reads an 84,000 byte file through 256 bytes. The check that matters is the second one: a small file read BOTH ways - whole with osFileRead and streamed - with the two checksums compared, so streaming is measured against the path already known to work rather than against a number someone wrote down. The checksum is Fletcher's rather than a sum, because a sum is the same whatever order the bytes arrived in and the order is exactly what streaming has to get right. Both checksums were also confirmed against the same arithmetic run on the host. The rest of the test is the cache: two files read alternately catch a memory that missed the name changing, and a rename catches one that missed the file moving - and that one would otherwise pass, since the blocks are still there holding the same bytes. The test file is generated rather than taken from the repository. The CosmOS sources would be a truer picture and would move the recorded checksum every time a line of CosmOS was edited, putting a real difference in a crowd of meaningless ones - the same trap the cycle counts used to set. cosmosBreak's recorded output moves by two bytes in two pointers: SbfsIndex added two bytes to the filesystem's data and Break prints the system addresses the registers happened to hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3b800a69e8 |
Monitor: a line assembler
a <address>, then instructions until a line that is just a dot. The syntax is the assembler's own: a selector rides on the mnemonic as LDA.0 or LDD.0.1, and leaving one off means Data Pointer 0 exactly as it does in a source file, so nothing learned at the monitor has to be unlearned when writing a program. Case is folded, since the assembler does not care either. Numbers are hexadecimal and bare. A source file writes 0x2000 or 0d16 because it has both and must say which; a monitor has one and says so once, in the manual, rather than on every line. It reads the same table the disassembler does, searched the other way round, which is the point of it being a table rather than two lists: what a writes, d reads back, and neither can drift from the other or from the assembler both were generated from. Instruction lengths come from the shared shape table too, so the cursor cannot get out of step with what was written. THE WHOLE LINE IS UNDERSTOOD BEFORE ANYTHING IS WRITTEN. Emitting the opcode first and discovering a missing operand afterwards leaves half an instruction in memory, which the next line usually covers up and the last line of a session does not. Written that way first and fixed. What cannot be written is a label, and that is the whole difference between this and the assembler proper: a label is a promise to fill an address in later, and later is what a line at a time does not have. The recorded test now types in a complete program - a string poked into Data Memory, instructions assembled into Program Memory, and the result run - and includes a lower case mnemonic, both selector forms, an instruction that does not exist and one missing its value, so the refusals sit beside the successes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c23adb2836 |
Break: name the status register properly and show the Stack Pointer
The dump labelled the status register "S", which reads as Stack to anybody sensible - and the Stack Pointer was the one register it did not show, so there was nothing to contradict the guess. It is written "status" now, and followed by the bits that are up, because a dump that makes you look the number up is only half a dump. The Stack Pointer is not in the frame, since the frame is where the Stack Pointer is. What the program had is fourteen bytes above it, that being what entering an interrupt puts down, so it is worked out and shown. Apps/Break.asm takes its second stop inside a subroutine, so the recorded output shows the Stack Pointer at FFFF and then at FFF5: a difference of ten, which is the size of a CALL frame. That checks the value is derived rather than constant, which the previous version could not have told you. Reported by Anachronaut, who read the output and asked why a pointer was two digits long. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5fd995aa62 |
Breakpoints: SWI osBreak, and s refuses a read only bank
A breakpoint that shows every register as the program had them, waits for a key, and carries on. NOTHING IS OVERWRITTEN, and that is the design rather than a shortcut. A breakpoint poked into a running program has to replace an instruction, and putting that instruction back in order to continue is the same act as disarming the breakpoint; firing a second time would mean stepping over the restored instruction and putting the breakpoint back behind it, and this machine cannot step a single instruction. SWI is two bytes, dispatches through a vector, and its frame already holds the address after it, so RETI resumes at the next instruction with nothing to restore and nothing to re-arm. It fires every time it is reached. The price is that a breakpoint is part of the program: a build with them in has different addresses from a build without. That is the bargain every machine with a break instruction makes. Every value shown comes out of the frame rather than the registers, because by the time the handler runs the registers are the handler's. Apps/Break.asm stops twice so that the second stop is checked as well as the first. Also here, found by the test that came with it: the monitor's s wrote into whichever bank was selected, and bank 2 is the controller's own table, published read only. Writing to it was refused, and a refusal nobody catches stops the machine - so selecting the bank table to look at it and then typing s killed the session. bankPresent now keeps the whole flags byte and s declines. The recorded output of cosmosMonitor had contained that crash, having been blessed without being read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |