0a2965bc638f93baab849e1a1c550e7e5037d616
53
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9c144469b4 |
Take the SplitLint findings that are one operation, leave the rest
Twenty four more sites, and the interesting part is which ones were left alone. A rule emerged while reading them and it held all the way through: apply where the repetition is INSIDE one operation, skip where the author's own structure says it is a new thought, and never where two equal values mean different things. Taken: - Five registers reassigned to a value they already held, where both are the same quantity: two masks in one expression in Snake, two spaces printed by the monitor, both halves of block zero in waitTest, and a RSTA in Pour that the very next instruction overwrote. - Eighteen SETDs that reload a pointer inside one operation - a store back into the variable just read, or an INCD stepping to the second byte of a two byte value. Those read correctly without the reload. - sbfsNext, which branched to the label on the line below it. Left, with reasons that are the useful part of this: - Eight registers where the same number means two different things. CosmOS and the loader set A to 1 for a blit command and then to 1 again for a bank number; Asm compares a type against 3 and then a status against 3. Removing those couples one quantity to another that is equal by accident and would part company silently. - Ten RSTAs that open the RSTA/RSTB/CCF/ADD "return zero" block. The redundancy is what makes that idiom self contained; taking it out makes the return value depend on the line above. - Eleven SETDs that begin an arm of a comparison chain. Each arm loads, compares and branches, and they get reordered - the repetition is the reason a new arm can be dropped in anywhere. - Twenty five SETDs separated from their pointer by a blank line or a comment, which is the author saying a new thought starts here. - Two CCFs before arithmetic, which this codebase writes unconditionally. - Three redundant branches in test programs whose recorded output includes addresses, where three fewer bytes moves what the test demonstrates. Nine recorded outputs moved and every one is a size in a listing or, for Life, five more generations inside the same cycle budget. Behaviour is unchanged everywhere: cosmosSnake and cosmosEdit pass byte for byte while Snake loses eight bytes and Edit twelve. CosmOS is 10,902 bytes of program against 10,937, and the native assembler 12,173 against 12,183. The CosmOS README's size for Edit moved twice in one sitting, and this morning's check caught it both times - which it could not have done before that claim was reworded to name what it was about. |
||
|
|
e1273337c4 |
Two mechanical fixes SplitLint found: MVQA, and RSTA for zero
Twenty four places moved Q into A or B by pushing it and popping it back. That is four bus cycles and two bytes to do what MVQA does in one of each, and several of them are inside loops - Life, the calculator, int8. Nineteen more loaded zero with INIA 0d0 where RSTA says the same thing in one byte. Both are equivalent at the CPU rather than by assertion: RSTA and INIA both leave Status alone, and PSHQ followed by POPA nets to A = Q with the Stack Pointer where it started. The one difference is that the pair leaves a copy of Q in memory just below the Stack Pointer and MVQA does not, which nothing here reads. Five recorded outputs moved and every one of them says the change worked: - 16x16Life fits five more generations into the same cycle budget, the first 457 lines identical, because the loop got cheaper. - Life.sbx is 1409 bytes rather than 1411, in three tests that list it. - Edit.sbx is 1995 rather than 1996. That last one broke a check I added this morning, and the hole is worth recording: the CosmOS README's claim about Edit's size did not have the word "Edit" on the same line as the number, because the subject was in the sentence before, so the check that measures quoted sizes skipped it silently. The sentence now names what it is talking about, which makes it both checkable and clearer, and the check fails on a wrong number there. Comments on either half of a replaced pair are carried onto the instruction that replaces them, so nothing anybody wrote was lost. |
||
|
|
8f4cc5878d |
Repair a table cut in half, and measure the numbers that had drifted
The README's emulator options table was split by forty lines of prose: two rows, then the whole discussion of the cost model, then five more rows with no header above them. Markdown renders that second half as something other than a table, so three of the seven options were not being shown as options. The rows are back together and the prose follows them. Four numbers had gone stale, in three different ways, and none was noticed: - "Five more scripts run alongside it" - there are six, and lint.sh had no bullet saying what it was for. - "rebuild all three tools" - there are four. - Files.asm quoted at 645 bytes in two places; it is 665. - Edit quoted at 1,983; it is 1,996. The last two are the most quotable sentences in the CosmOS README and the least likely to be rechecked by hand: the programs kept being made better and the sentences about how small they are stayed where they were. So docs.sh measures all four now. It counts the scripts in Tests/ that are not the driver or the disk builder and checks the README says that many and explains each one; it counts what the makefile's all target builds and checks the tool count in both phrasings, which took two attempts because one sentence says "the four tools" and the other "all four tools"; and it assembles every app the CosmOS README quotes a size for and compares. Each check was confirmed by making the fact wrong and watching it fail. WAIT also added a second kind of cycle this morning and the cost model section still described only one. It now says what an idle cycle is, why the two are counted apart, and what that distinction is FOR - a machine that slept through a slow disk and one that spun on it take the same elapsed time and print the same characters, and only the split tells them apart. The duplicated sentence about pipelining is gone; it was said twice, eleven lines apart, in nearly the same words. |
||
|
|
c216c83e12 |
The filesystem waits for the disk instead of asking it repeatedly
sbfsWaitDisk spun on the status port. Read ahead had already hidden about
three quarters of the latency, and what remained was still 11.5% of Type
over a 14K file on a ten thousand cycle disk - all of it memory traffic
spent finding out that nothing had happened yet.
It tests the port first and only waits if the disk is still busy, and that
order is the whole of what makes it safe: the disk raises its line when it
finishes, so a disk that finished in the gap between the test and the WAIT
has its line standing already and the WAIT does nothing rather than
sleeping through the answer. No handler and no vector - the shell keeps the
Interrupt Flag down, and a WAIT wakes on a line whether or not anybody
means to answer it.
Printing a 14K file, by where the cycles go:
cycles a block total bus waiting
0 922,570 922,570 0
2,000 946,474 922,702 23,772
10,000 1,042,474 922,702 119,772
The bus column stops moving. What the program costs in memory is now the
same whatever the disk does. On this emulator nothing observable changes;
on hardware it is a CPU standing out of the way of the memory controller
rather than competing with it for every one of those 119,772 cycles.
The first version cost 660 cycles more at latency zero because it read the
status port again on the way out. AND writes to Q and leaves A alone, so
the status was already there - which is what the original said in its own
comment, and what I stopped believing while rewriting around it.
|
||
|
|
c3188ed657 |
Seventy becomes seventy one: a machine that can wait
HALT is terminal - stepCPU returns at once when the Halt Flag is up, so a halted machine does not execute, service devices, or take an interrupt - and that has to stay true, because every test ends with a halt and "halted" is how a program says it has finished. The consequence was that SplitBit had no way to wait at all. Every wait was a spin, and a spin is bus traffic: 11.5% of Type over a 14K file on a disk of ten thousand cycles, after read-ahead had already hidden three quarters of the latency. WAIT is 0xFE, one byte, no operands, sitting under HALT where the instruction that almost stops the machine belongs. Three decisions in it: - A line already standing means there is nothing to wait for, so WAIT does nothing. That is what makes test-then-wait race-free. - Any line ends the wait, masked or not, so a program can sleep on a device it has no handler for and read its status afterwards. Masking says who answers a request, not whether it happened. - A line that wakes the CPU without being dispatched is taken down by the WAIT. Left standing it would be found by the next WAIT, which would return at once - the program would spin exactly as before while looking as though it slept. Waiting is NOT a Status bit, and that is the trap avoided rather than a gap: Status rides into the interrupt frame and comes back out, so a machine interrupted mid-wait would return from its handler still waiting, and wait again for what it had already been given. An internal field instead. Idle cycles are counted apart from bus cycles and the halt line says so when there are any, which is what makes the difference observable at all - with the line-clearing removed the total moves by ONE cycle, 20,100 against 20,099, and only the idle half changes, halving to 9,976. A test on totals could never have seen it. Tests/terminal.sh asks that question, being the file for things a recorded output cannot see, and fails with the clear removed while "both reads finished" still passes. Three collisions, all found by building it: - 0xFE was the assembler's "not an instruction" sentinel. getOpcode now answers a negative NOT_AN_OPCODE, which is outside the range of every possible answer instead of inside the unused part of it. - 0xFE was also what faultTest and faultResumeTest executed to provoke a fault. They now use 0xFD and say why, because they did not fail when it became an instruction - they HUNG, having started sleeping instead. - Keys.asm has had a label called "wait" for a year, and mnemonics are matched uppercased. What that reported was "Branch without label" at the BRQ thirty lines away. The assembler now refuses a label that is already an instruction, at the label, by name; every instruction added takes a word out of the space of label names, so this will happen again. |
||
|
|
6b41354f8f |
Delete a comment describing a design that was removed, and check the rest
Two comments in the native assembler survived the changes that made them false, and both are the kind that misleads rather than merely ages. Asm.asm still explained an eighteen kilobyte buffer that the whole output had to fit in "because a file is written in one call and there is nowhere to put half of one" - which stopped being true when the assembler learned to stream, and the variable it described, ImgRoom, does not exist any more. It was sitting in front of the comment that replaced it, so the paragraph a reader met first described the design that had been taken out. Replaced by what is actually there: no limit but the disk, one block at a time through a window in scratch. scratch.asm said the system keeps below 0x1000 twelve lines above the paragraph explaining that the system's half of Data Memory was doubled to 0x1FFF. A stale number next to its own correction is worse than a stale number alone, because which one a reader believes is a coin toss. docs.sh now checks both of the facts scratch.asm asserts about the machine around it: the floor it claims the system keeps below, against the CosmOS README, and the address its buffers start at, against where the assembler's own data actually ends. Neither is enforced by a line of code anywhere - the map is a comment, deliberately, because reserving the buffers would put 22K of zeroes in the file and the assembler could not load itself - so a check is the only thing that can hold them. Both fail when broken; the second reports the exact overlap. |
||
|
|
ce0f18f4ef |
Refuse a directory whose last entries cannot be named as a parent
A parent is an entry index PLUS ONE in two bytes, so entry 65535 has no parent number: adding one wraps to zero, and zero is the root. Eight entries to a block, so 8192 directory blocks reaches it and SplitDisk formatted that happily. It does not fail by refusing, which is why it was worth chasing rather than reasoning about. Reproduced on a disk built for it: mkdir /deep/child, with /deep at entry 65535, printed 'Made "/deep/child" as entry 0' and put child in the ROOT. Listing /deep then showed nothing, because the search is for a parent of 65536 and the entry carries zero - so the same mkdir succeeded again, and again, and five entries called /child piled up in the root. Duplicate names in one directory are the one thing rename refuses outright, on the grounds that a search answers with whichever it meets first and the rest can never be reached; this manufactured them one per attempt. 8191 blocks is the most, giving 65528 entries. Refused when formatting and again when reading, in both implementations, because a disk claiming more was made by something that never checked. On the machine only the high byte of the count has to be looked at: anything from 0x20 up is too many. Three checks, all of which fail with their guard removed. The machine's disk claims the size rather than having it, so the test image is 64 blocks that lie rather than sixteen megabytes that do not - mounting is refused at the geometry, which is read out of block 0. |
||
|
|
2b5506ee70 |
Stop the prompt writing off the front of its own buffer
The prompt is the working directory's path, worked out each time by walking the chain of parents up to the root. The names arrive deepest first, so they are written backwards from the end of a 127 byte buffer - and nothing bounded that walk. Nothing bounds the depth either. A path given to one operation is capped at 95 characters and a 22 character name, but "mkdir a" and "cd a" are each far inside that and can be repeated forever. Six directories of 22 characters is 132 characters of path, and at that point the walk wrote down past the front of CwdText and into what the assembler had laid out below it: the shell's own command names. ExitName sits five bytes under, so the word "exit" went first and the shell stopped recognising the command for leaving. Measured, not deduced: fine at five levels, gone at six. The walk now counts the room it has left, byte by byte, and stops. What is already written is the DEEP end of the path, which is the end worth showing, so it is cut at the front and three dots say so - out of three bytes held back from the count, so there is always somewhere to put them. Twenty levels deep the prompt shows the last five and every command still works. cosmosDeep records that, and records it by running help, cd and exit from down there rather than by looking at the prompt: a wrong prompt is cosmetic, and this was writing into other variables. It fails with the bound removed. The tree is built by SplitDisk because a path that long cannot be given to mkdir in one piece - which is the same fact that makes the depth unbounded. The three path limits are written down in the README now, including which one actually binds. The other two do not: the longest path on a full install is 21 characters. |
||
|
|
ce8fb721fe |
Say a temporary is one in the entry, not in its name
Saving something that already exists writes a temporary, deletes the original and gives the temporary its name, so that nothing is lost if the writing fails. The temporary was told apart from a real file by being called sbfs.part or sbfs.out - and those are legal names. Starting a save deleted whatever answered to one as stale scratch, so saving anything at all in a directory destroyed your own file of that name there, silently. Flag bit 0x04 now says it. The property is not in the contents - the same bytes become the finished file the instant the rename lands - so it belongs in the entry, which is the thing the commit changes. sbfsCreateTempAt is the door temporaries come in by, the commit writes the flags flat along with the name, and cleanup wipes what it finds only if the entry says it is ours. Anything else stops the save instead. The bit is also the recovery. Both listings show an unfinished write rather than sizing it, because the size in the entry is the room that was asked for and not what was written: "<unfinished>" from dir, and a line from SplitDisk saying the blocks are held and a rename brings the data back. That was the gap in what the last commit documented - the data survived a crash and nothing would show you where it was. Four new agreement checks, three of which fail with the guards removed. The fourth needed rebuilding first: both tests started on one disk, and the first save ate the sbfs.part that was the second test's SOURCE, so the copy failed for want of a file, never opened a stream, and passed while reporting on nothing. A disk each. The fifth check forges the wreckage by setting the flag on a finished file, since nothing here can crash a save half way through. No version bump: a committed file never carries the bit, so a disk this writes is byte for byte the disk the old code wrote, which the whole-image comparisons confirm. Only the wreckage differs, and older code reads that as an ordinary file - which is what it did before. |
||
|
|
9e2aa0122e |
Say what a safe save actually promises, and where it stops
Both sbfsSaveFile and sbfsStreamDone write a temporary, delete the original and rename the temporary onto its name, and the README explained why that order and not the obvious one. It never said what the resulting guarantee is, which invited the stronger reading: "safe save" sounds like it survives anything. It survives everything that goes wrong while it is running - no run long enough, no free blocks, a refused write, a name that turns out to be a directory, a writer that gives up - and in all of those the original is untouched. It does not survive the machine stopping, because the commit is two block writes with a gap between them: stop there and the old file is gone and the new one is still called sbfs.part. The bytes are all present and one rename by hand recovers them, but nothing does that on its own. Written down in the README beside the ordering it qualifies, and at both commit points in sbfs.asm, where a reader is when the question occurs to them. Closing the gap wants a journal or a second directory, which is a lot of disk for two writes; a boot-time consistency check is the cheaper answer if it ever matters, and it would want temporaries flagged in the entry rather than recognised by name. |
||
|
|
aa7bdc6acd |
Check the memory map against itself, not only against the code
The CosmOS README's Data row gave the system 0x0000-0x3FFF and a loaded application 0x2000 and above: two columns of one row that cannot both be true. Program was doubled to 0x3FFF when CosmOS outgrew its first map and that number was copied into the Data row as well, where the answer is 0x1FFF. docs.sh measured both segments against the CosmOS column and passed the table anyway, because it never read the column beside it. A number checked against the code and not against the number next to it is still unchecked, so it now reads both and compares them - and compares two further copies of the same fact that had gone stale on their own: the minimal application in the README, still based where applications lived before the doubling, and the map cosmos.asm opens with, which somebody reading the system reads before they read the README. Each of the three checks was confirmed by breaking the fact and watching it fail; the first reproduces exactly the text this commit removes. While in that header, the command list said five commands and CosmOS has eleven and a search path besides, and "dump is next" outlived the monitor. |
||
|
|
db3d349da8 |
Read the next block while the program is busy with this one
A file is read front to back, so when something asks for a block, the one after it is almost certainly wanted next. sbfsReadOne asks the disk for it straight away and hands back the block that was wanted - so the transfer happens while the caller is busy, and the waiting is mostly over by the time it comes back. Printing a fourteen kilobyte file: 1,064,498 cycles at two thousand a block becomes 976,882, and 1,576,562 at ten thousand becomes 1,032,889. The second figure barely moves between those - seven per cent from an instant disk to a slow one, where without it the same change costs sixty eight. A machine that reads ahead stops caring very much how fast its disk is. NOT FOR DIRECTORY SEARCHES, and that was tried first and thrown away. A scan stops the moment it matches, so the block it would read ahead is one nobody will ever look at - a transfer to fetch and another wait to throw away. It was nineteen per cent SLOWER on a lookup at ten thousand cycles a block. Reading ahead is a bet that the next block is wanted, and a search is exactly the case that hopes it is not. The scan loop is untouched. Three per cent is what it costs when the disk is instant, which is the bookkeeping with nothing to hide behind it, and the default. The disk has one buffer, so the only way to know what is in it is to remember what was last asked for. Every read records that; a write clears it, because a write fills the buffer from memory and no read asked for what is in it. Getting that wrong would blit whatever happened to be there and call it the block somebody wanted. The read ahead is bounded by the file's own length, so reading the last block does not fetch whatever follows the file on the disk - which belongs to somebody else and would be paid for twice, once to fetch and once to discard. It cost one bug, and an instructive one: the next index was worked out by stepping SbfsIndex, which is the CALLER'S and is still wanted after the return - handleFileBlock compares it against the file's length to see whether this was the short last block. Every block reported the wrong number of bytes, and the output of Type skipped five lines in the middle. It has a place of its own now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d4cba36c5e |
Devices that take time, and a filesystem that waits for one
The disk's status has always had a bit meaning "still going", and the header beside it has always said to honour it. Nothing did, because nothing could: the host finished the transfer inside the instruction that asked for it, so the bit could never be seen up and asking about it was asking about something that cannot happen. --disk-cycles gives it a latency. The command is still checked at once, because a refusal is not work - a block that is not there fails before any head moves - but the transfer is remembered and done when the machine has run that far. Until then the buffer holds the block BEFORE this one. That last part is the point. A program that does not wait gets the wrong bytes rather than an error, which is the failure the bit exists to prevent and the one that would never have shown up. With a latency of two thousand, CosmOS could not even mount: sbfsMount reads block zero and looks straight at the buffer. deviceTick is the general shape rather than a disk feature. Called once per instruction with the machine's clock, it lets anything whose moment has come finish - which is what a display that refreshes, or a port that waits on the host, would want in exactly the same way. The filesystem watches the bit now, in one small routine reached with RCAL. That is not decoration: what it hands back is the settled status in A, and CALL puts A back the way it found it, so an ordinary call cannot carry the one thing this exists to carry. Two bytes of Stack rather than ten, in a routine that runs on every block the machine ever touches - the first place in the system where the new call is the right one rather than merely a cheaper one. The manifest takes a @N after a disk, the way it already takes :ro, so a test can ask for a slow one. cosmosSlowDisk lists a directory at two thousand cycles a block and gets the same listing as everything else, which is the whole assertion: a filesystem that did not wait would print nonsense rather than fail. Zero is the default and every other test runs at it. What waiting costs, on a directory heavy run: 229k cycles at zero, 275k at five hundred, 415k at two thousand, 1.16M at ten thousand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
54f5cfe8a4 |
Add Copy and Compare, which stream in both directions at once
Copy joins the read and write streaming services: source and destination are both larger than Data Memory while the program owns one block. Compare reads two files through separate blocks and ignores the bytes past a short final block, which belong to neither file. Between them they exercise empty, exact-block, part-block and 84,000 byte files, and the host extracts the copy afterwards so that two native programs agreeing with each other is not the only oracle. Written by ChatGPT, as their headers record, along with the agree.sh section and the manifest entry that drive them. THIS SHOULD HAVE COME FIRST. The commit before it staged whole files rather than the hunks it meant, so its manifest already names these two programs while their source was still untracked - that commit will not build on its own. Left in place rather than rewritten, since the pair is right and only their order is wrong. NOTES.md is their review of the streaming work. The first item in it is fixed by the commit before this one; the rest are still open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a7d3e09d94 |
Refuse a streamed file that commits more than it reserved
osFileStart sets an extent aside and osFileWrite refuses a block index outside it, so writing off the end was already barred. Committing a larger size was not, and reaches the same neighbour by simply claiming it: a directory entry is the only record of what a file owns, so an entry claiming a block it was never given owns it, and so does whatever owned it before. Both files then look perfectly well formed. The free count went backwards past zero on the same path. Found by ChatGPT's review of the streaming work, in NOTES.md. I had bounded the index because writing off the end was the obvious way to reach a neighbour, and had not noticed that the other end of the same reservation was open. THE SIZE IS COMPARED, NOT THE ROOM IT TAKES UP. One block and a tail occupies exactly what two whole blocks occupy, so bounding the blocks alone would let a file reserve the first, commit the second, claim no block it was not given, and still report two hundred and forty six bytes that were never written to it - whatever the disk had there before. Checked before anything is touched, which is why the temporary is found twice. The old file is deleted a few lines down and a refusal after that point would have destroyed the thing it was protecting. AND IT CAUGHT A REAL ONE IMMEDIATELY. The assembler reserves the file plus room for its vectors, and asked for four bytes per vector DECLARED - which looks like a safe bound and is not, because a device is declared during the SECOND pass, in the line that implements it. A program with a device installs a vector that was not counted when the room was measured. CosmOS reserved 14,163 bytes and committed 14,167, writing four bytes past what it had been given on every build since S2. It landed inside the last block it owned, and would not have if the boundary had fallen four bytes earlier. It reserves against the vector table's LIMIT now, which cannot go stale whenever things are counted. Claim.asm is the program that tries it: reserve one block and a tail of ten, write them, then tell osFileDone the file came to two whole blocks. The refusal and the honest commit that follows are both recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
af0360128b |
Sixty four instructions becomes seventy
The six settled back on the twenty fourth, built now. RCAL and RRET are a call that puts nothing back. CALL restores A, B and Data Pointers 0 through 2, which costs ten bytes of Stack and is why a subroutine here can only hand anything back through Q, DP3 or memory. RCAL costs two and restores nothing, which is what a short leaf routine wants and is unsafe in exactly the way the name says. They are a pair because the frames are different sizes: returning from one through the other walks the Stack to somewhere that was never a return address. That was the user's correction to the original proposal, which had a raw call and no raw return. DPUA and DPDA offset a Data Pointer by A; DPUW and DPDW by A and B together, most significant first. DPUP and DPDN take a byte written into the program, so moving a pointer by something just worked out meant storing it and loading it back. Down as well as up on symmetry grounds, which was also the user's call - the argument against it came from counting uses in a corpus written under the constraint. The opcodes sit where they belong: 0x16 and 0x1E immediately below CALL and RET, and 0x4E through 0x51 at the end of the Data Pointer family. All six fit shapes that already existed, so instructiontable.py needed only set membership and both machine side copies of the table regenerated from it unchanged. Checked at every level it exists at: the emulator runs them, the host assembler encodes them, the monitor disassembles all six with the right lengths, and the assembler that runs on the machine builds a program using them byte for byte identically to the host - and that program runs. The recorded test measures what the two calls COST as well as what they put back, because an RCAL that quietly did what CALL does would still return to the right place. It does not survive that: returned through RRET, it hangs. docs.sh can read a two word number now. The count of instructions taking a Data Pointer went past twenty, and the pattern only allowed one word, so the check would have reported that the manual had stopped saying it rather than that the number was wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
00d896e3e7 |
Break shows registers it chose, not ones it inherited
Break left DP1 and DP2 alone, so what a stop printed for them was whatever the shell happened to have left there - which is a CosmOS address, which moves whenever CosmOS is touched at all. The recorded output had to be taken again four times in one day's work, every time for a value that is not this program's and that nothing should ever depend on. It sets all three of the pointers it owns now, rotated between the two stops so that every one of them visibly changes, the way A and B already did. DP3 stays as the system left it: it is where the program was entered, which is the one thing worth seeing here that this program did not choose, and it is steady because it is this program's own base. A demonstration of what the registers were should show registers somebody chose. Then every line of the record is being asserted rather than merely observed, and a reader can tell which is which. Checked both ways: sixty four bytes added to CosmOS's data no longer moves it, and reading the frame at the wrong offset still fails it. cosmosRun and cosmosMonitor move because Break is sixteen bytes longer and both of them list the disk it sits on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fb7b224bbb |
S2: the assembler writes the file as it makes it
The output image is gone. It was eighteen kilobytes and it is now one block of window, because the file was always produced in order and only ever needed to be written that way. Everything works in FILE OFFSETS now. A cursor is a two byte number counting from the front of the file, and since a block is two hundred and fifty six bytes, the block it lands in is the offset's high byte and the place within that block is its low one - so there is no division anywhere, and ImgWalk, ProgPut and DataPut needed no change but where they start. ONE WINDOW RATHER THAN THREE. The plan said three: one per segment, and a third for the block where the program ends and the data begins, which belongs to both. Fetching a block back instead makes all of that one case. The header is patched after every byte is out, the boundary block is written by both cursors, and both are simply revisits - a revisit is what fetching handles. osFileFetch is the service that allows it, and is the read side of the write. A run of bytes in one segment costs nothing extra; a switch between segments costs two block operations, and a source file has a few dozen switches and several thousand bytes. Two bugs, both a pointer meaning two things: putAt took the cursor to advance in DP2 and then wanted DP2 for the window's address. A call puts DP2 back the way it was AT THE CALL, so the step at the end moved whatever the last call had left there - the window walked off across memory while the cursor stood still. It goes in memory now, like the block did in S1, and for the same reason. The size the file is created at could not be right. How many vectors are actually installed is not known until the second pass has resolved their handlers, and by then the file must already exist to be written into - so Keys, which brings one vector, came out four bytes short. Teaching the first pass to count them meant teaching it about devices, and about a Boot line in a loadable program not being installed at all, which is two ways to disagree with the second pass about what a file contains. So osFileDone is told the size instead. A writer asks for as much as the file could possibly come to - the whole of it plus four bytes for every vector DECLARED, which no file can exceed - and says what it really came to at the end. The blocks it did not use go back to the free count. Asking for too much costs a moment; asking for too little writes off the end of a file. That is a better service for it, not a workaround. A writer that cannot know its size until the last byte is the ordinary case, and it is exactly the case this whole rung exists for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
9f7dffdeca |
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 |
||
|
|
7cd5e34347 |
Two ceilings a hundred bytes apart look like one ceiling
The sixteen kilobytes taken back a moment ago all went to the output image, because that was the wall: 13,245 bytes of cosmos.bin against 13,312. Lifting it moved the machine straight into the next one, a hundred and eleven bytes away - the label names, at 8,081 of 8,192 - and the index was a hundred and eighteen entries from the same place. So the room is shared out rather than given to the obvious one. Names and index both double, and the output takes what is left, which is still four and a half thousand bytes more than CosmOS needs. LabLimit and LabRoom in labels.asm have to agree with the map in scratch.asm and are now said to. Worth recording how this was found, because it is the good case. The assembler STOPPED and said "no room left for label names: Mode, at line 3598" - a limit it checks, names, and points at. Every other ceiling this project has hit went unnoticed until something downstream broke: a program loaded over the shell, a path silently cut short, a file reported as itself less 65,536. A limit that announces itself is worth the handful of instructions it costs. The output's eighteen kilobytes are temporary. They exist because the assembler holds a whole finished file in memory before writing it, and the file is produced in order, so it could be written as it is made. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ec56439d9a |
Take back the sixteen kilobytes the scratch map left behind
The assembler's buffers start at 0x8000, and the reason written beside them is that everything above its own data is free. That was true when it was written. The system kept below 0x1000 then; its data now reaches 0x1FFF, and the assembler's own moved from 0x1000 to 0x2000 with it when both halves of the memory map were doubled. The floor came up and the map stayed where it was, leaving sixteen kilobytes between the two that nothing touched at all. Starting at 0x4000 takes that back, with nearly four kilobytes of slack still in front of it and room for the assembler's own data to double before the two would meet. All of it went to the output image, because that is the buffer that is actually full. cosmos.bin is 13,245 bytes against the 13,312 it had, which is sixty seven bytes - the next thing added to the system would have made the machine unable to build it, and it would have shown up as a test going red rather than as anything explicable at the machine. The label table has room yet, so this is not the moment to share the space out evenly. This is the third time a limit here has gone unnoticed until something hit it, and the second where the cause was a number that stopped being true rather than one that was ever wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
06bdbf7728 |
D5: move in, and give the assembler somewhere to look
The demo disk is three directories instead of thirty nine names in one list with cosmos.asm sitting between fileStream.asm and sbfs.asm: /Apps what you run /Source what you name to the assembler /Lib what those include The split is by ROLE rather than by which directory the host keeps a file in. Everything in /Lib is named by an #Include somewhere and by nothing else, which is what makes it a library rather than a source. THAT LAYOUT WAS NOT POSSIBLE UNTIL NOW, and finding out why is what this rung actually cost. An include on the machine was a bare name resolved where you stood, so every source that calls a service had to sit in the same directory as services.asm - which is every source worth having. The first arrangement of this disk put the examples in a directory of their own and none of them would assemble. So the native assembler has a search path: beside you, then /Lib. The same rule the shell already uses for a program it does not recognise, applied to the thing that reads source, and the same reasoning for it being two fixed places rather than a list - a list needs somewhere to live between one boot and the next, and there is no such place yet. It also brings the native assembler nearer the host one, which has searched -I directories since before there was a machine to run this on. The reader's per-file state grew from 293 bytes to 301, because the name it keeps is a path now and every block of a file is asked for by it. Six of those would no longer fit the room set aside, so the include list moved up a page. Both numbers are written down in two places on purpose and both were changed. dir said cosmos.asm was 17,460 bytes. It is 82,996. The size came out of the block count's LOW BYTE shifted up and the tail beneath it, which is sixteen bits, so anything from 256 blocks upward came back as itself less 65,536 - a plausible number, and wrong. Files that big say their size in blocks now. Printing the true figure wants decimal printing twenty four bits wide, which is a page of console.asm to say something nobody reads more precisely than "big". The Assembler Manual's line about SBFS being flat was the last thing in the repository still claiming it, and docs.sh now looks for that phrase and three like it in all four documents. Not a section that is wrong - one clause inside a paragraph that is otherwise right, which is the shape this kind of staleness takes. The duplicate puts are gone with the wildcard that caused them, so building the disk is quiet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
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 |
||
|
|
dbe58db660 |
Add Type and More, and the file stream they are built on
Two applications that read a file too big for Data Memory: Type prints one, More pages it. Both sit on fileStream.asm, which wraps osFileInfo and osFileBlock into open-and-next so an application walks a file's blocks without repeating the service calls. The disk fixture is deliberately awkward: readable.txt crosses several blocks and carries no zero byte to be mistaken for an end marker, and empty.txt says that zero blocks is a valid file rather than an error. These three files were written by ChatGPT, as their headers record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4fd8bf7b3f |
Step a Data Pointer with INCD and DECD, not DPUP and DPDN by one
DPUP takes an immediate, so an offset of one is legal and does exactly the right thing. It is also three bytes where INCD is two, and reads as "offset the pointer up by one" where INCD reads as "step the pointer". 56 of them across 15 files: the system, the assembler, the editor, and eight test programs. CosmOS is 9,564 bytes to 9,537, the native assembler 11,648 to 11,635, and every program in the repository together 49 bytes lighter. The worst offender was numbers.asm, written this week, where every sixteen bit helper reaches the low byte and comes back the long way round. It is the file every other part of the assembler includes, so it is the first thing anybody reads when they go looking - and it was teaching them the long way. Pattern matched off sbfs.asm rather than off the instruction table I had just embedded in two programs. THIS IS NOT TWO WAYS TO DO ONE THING. DPUP takes an arbitrary number, so one is inevitably among them; INCD earns its place by making the common case a byte cheaper. The overlap is structural and the choice is a usage question, which is a linter's job rather than an ISA's - "DPUP.n 0d01: INCD.n does this in a byte less" is a mechanical rule with no judgement in it. Nothing needed re-recording, which was not a foregone conclusion: cosmosBreak prints the system addresses the registers happened to hold, and they did not move. Both assemblers still produce identical bytes and CosmOS still builds itself to a fixed point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3f95056eec |
Three papercuts a visitor would hit first
Found by a review, all three confirmed by trying them rather than by reading. 1. THE STRICT BUILD CLAIM WAS FALSE. The README says the sources build clean under -std=c11 -pedantic with -Wall -Wextra, and they did not: realpath is an XSI interface, and _POSIX_C_SOURCE=200809L does not reach it, so the assembler would not compile. The ordinary -Os build never saw it, because without -std=c11 the compiler's own default declares realpath anyway. _XOPEN_SOURCE=700 is POSIX.1-2008 plus XSI, and covers every file on its own. Narrowed by compiling each source with each candidate macro rather than by adding one and hoping. And 'make strict' now checks it, as part of 'make test'. The README makes a claim somebody may check by typing it, so the suite types it. Verified the check bites by putting the old macro back. 2. THE COSMOS README HAD NOT CAUGHT UP WITH THIS WEEK. It said there were no breakpoints and proposed writing a spare byte over an instruction - two commits after SWI osBreak was built, which does it without overwriting anything and is described correctly further down the same file. Its Current Scope said there was no native assembler and that self-hosting was the intended long-term milestone. And the streaming section spoke of a machine "one day going to assemble itself". All three now say what is true. Self-hosting is described as done, with what is left of it named: a linker, and an editor that knows what assembly is. 3. 'make run-hello' DID NOT WORK, and it is the one command the makefile's own header advertises. It was a pattern rule against $(BUILD)/%.bin, which worked while every program sat at the top of Programs/ and broke the moment they were filed into Examples/ - which I did, four commits ago, without trying it. The name is looked up among the programs now, so it is the program's name rather than its path, and an unknown one lists what there is instead of saying "No rule to make target". The same sweep was run across all four documents for other claims this week invalidated. The remaining "not yet" phrases are about faults that genuinely are not defined yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
7e8639489b |
Take the direction out of the last cross references
Includes two edits of Anachronaut's: dropping a "See the Assembler Manual" from text that is now IN the Assembler Manual, and tightening a sentence about when a vector number has to be agreed. The first of those was mine to have caught. Moving "Loading A Program From A Disk" into that manual turned a cross reference into a document telling you to go and read itself. I checked for dangling references before the move and found none, because I checked for references by SECTION NAME and not for references to the manuals themselves. Checked properly now, both directions: no other document refers to itself, and nothing that moved says "above" or "below" about something that ended up in a different file. The three remaining directional references are gone with them - two that were correct today and one, "See Numbers You Write Down below", pointing at a section twenty four lines further down that nothing guaranteed would stay there. A reference that carries a direction is a reference that goes wrong the next time anything moves, and this week moved a great deal. There are none left anywhere in the repository. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
b6004bdcde |
Say "boot image" where that is what is meant
"Binary" was doing three jobs. It meant an SPBT file that the machine starts from; it meant whatever the assembler happened to produce, which is now either that or a loadable program; and it meant a compiled host tool. A word that means three things means none of them, and the first of the three has a name already - this project has been calling them boot images for a while and the manuals had not caught up. Where it means an SPBT file -> boot image Where it means either output -> output Where it means a host executable -> left alone Where it means base two -> left alone The user facing messages move with it: Error: No boot image specified. Usage: ./SplitBit [OPTIONS] <boot image> Error: This is not a SplitBit boot image. Error: This boot image is in format version 2, and this emulator reads 1. Successfully wrote SplitBit boot image to "hello.bin". The assembler's own help was the interesting case. Its -o writes either format, so "the binary" there was never right - it is "the output" now, and the message that names the format is the one that says which it wrote. No recorded output contained the word, so nothing needed re-blessing. Checked before starting rather than after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fa3982dbd9 |
Move the CosmOS third of the Programming Manual to CosmOS
386 of the manual's 1,116 lines documented an operating system rather than a machine. The split inside that file was never tutorial against reference; it was the machine against the software that happens to run on it. What A Program May Ask The System For 129 -> CosmOS README Programs That Come With The System 111 -> CosmOS README Reading And Writing The Filesystem 64 -> CosmOS README Loading A Program From A Disk 52 -> Assembler Manual The Console Library 25 -> CosmOS README The services are the clearest case: a hundred and thirty lines describing what CosmOS offers a program, in the manual for a CPU that has no operating system of its own. A different system on the same machine would offer different services and that section would be wrong for it. The loadable program format goes to the Assembler Manual instead, because SBEX is a thing the assembler WRITES. Nothing in the CPU knows what it is. The Programming Manual is 716 lines and fourteen sections now, all of them about the machine. TWO DUPLICATE DESCRIPTIONS COLLAPSED INTO ONE EACH. The application list existed in both documents in different words, and the CosmOS copy had gone stale - no Break, no Stream, no assembler - because only the manual's copy was checked. Moving the checked one in and deleting the other leaves one list, and docs.sh follows it. The second was made by this commit and caught while reading the seams: the CosmOS README already had a service table, so the move briefly produced two. That section now says what services are for and points at the one table. Renaming a section as it moved: "Reading And Writing The Filesystem" is "The Filesystem Library", which says what it is and reads beside "The Console Library". docs.sh follows all five, and each was verified by renaming the heading in its new home and reading the complaint. The README and the CosmOS README both described what the other manuals cover, and both were wrong the moment this landed; they say the division out loud now, since it is the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
6dbb38b209 |
Stop committing assembled programs, nine of which had gone stale
Thirteen build artifacts were tracked: ten .bin files, a disk image, and the text file that had been put on it. Every one is produced by 'make' in Programs/ from a source file that is also tracked. NINE OF THE TEN BINARIES DISAGREED WITH THEIR OWN SOURCE, including Programs/CosmOS/Bin/cosmos.bin - the operating system. Only hello.bin still matched, and that by accident. That is worse than shipping no binary at all. A repository holding both a source file and a stale build of it has two answers to what a program does, and the wrong one is the one that runs. Somebody reads the source, runs the binary, and sees behaviour that is not in the code with nothing to tell them why. Nothing referenced any of them - not the makefiles, not the tests, not the manuals. Checked before removing, and checked after: the suite passes, and a clean clone with all of them gone builds both tools and assembles every program from source alone. .gitignore now covers *.bin, *.sbx and *.img, unanchored, because a build output may appear anywhere a source file lives. Programs/CosmOS/Bin/ held cosmos.bin, disk.img and a 17 byte hello.txt that existed only to be put on that image. It was a snapshot of a demo from before Tests/makedisks.sh built its own fixtures, and it goes with them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fb335681d2 |
M4: SplitBit assembles SplitBit, and then assembles itself
> load Asm.sbx
> run cosmos.asm
wrote cosmos.bin: program 7036, data 2448, labels 475
> run Asm.asm
wrote Asm.sbx: program 7533, data 4099, labels 555
Both byte for byte identical to what the host assembler builds from the
same source. The machine now builds the operating system it is running on,
and builds the thing that built it.
THE CHECK THAT MATTERS MOST IS THE THIRD ONE. A binary that matches could
still have come from an assembler wrong in some way this particular source
happens not to exercise. So Tests/native.sh boots the CosmOS that CosmOS
built and has THAT assemble CosmOS again - and the second generation is
identical to the first, down to the cycle count. It is a fixed point: the
machinery has been through itself. After this the host is a convenience
rather than a necessity.
WHAT STOOD IN THE WAY was not the assembler. It loaded, faulted at 7,780
cycles, and the fault was in CosmOS: a loaded program is staged at 0x8000
before being blitted into place, so the whole FILE has to fit in the 32,768
bytes above it. The assembler's file was 33,983, and 22K of that was
zeroed scratch buffers - because #Reserve emits what it reserves.
None of that is initialised data. It is scratch, wanted only while the
assembler runs, and while it runs everything above its own data is free.
So the buffers are a MAP now rather than declarations - Assembler/scratch.asm
writes down six addresses and the file carries none of it. 33,983 bytes
became 11,648, and the assembler could load itself.
The map has a file of its own because the reader and the label table both
need addresses out of it while neither includes the other.
The sizes are cut to the largest thing it is asked to build, and that turns
out not to be the operating system: the assembler is 555 labels and 11,648
bytes of output against CosmOS's 475 and 9,564. The hardest thing this
assembles is itself.
Also: sizing it for CosmOS meant raising the label table, and raising the
label table is what pushed the file over the staging limit. The two facts
only met because the first one was tried.
Speed, measured rather than guessed: CosmOS takes 80,168,646 cycles, which
is eighty seconds of emulated time and under a second under --fast. Most of
it is a straight walk of 475 label names, several thousand times. Sorting
or bucketing that is easy and was deliberately not written before there was
something to measure.
make run-cosmos now puts every source file on the disk, so the whole thing
can be done rather than read about.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
20989c3439 |
M3: programs that bring their own vectors
> load Asm.sbx
> run Keys.asm
wrote Keys.sbx: program 558, data 85, labels 52
> load Keys.sbx
> run
keys, by interrupt. q stops.
ab
the console has been handed back
The machine assembles a program carrying an interrupt handler, the loader
installs its vector, the console interrupts into it, and the shell takes the
vector back at exit. Byte for byte identical to the C assembler's, and
Tests/native.sh now checks a boot image and four loadable programs on every
run.
WHAT IT TOOK:
A declaration and an implementation are the SAME ENTRY. services.asm says
a service is called osPrintString and has number 16; cosmos.asm says
osPrintString is handled by handlePrintString. The name is met twice and
the second time fills in the handler, which is what lets one shared file
serve both the caller and the implementer.
So the first pass declares and the second implements. That is forced: a
handler is an address, and no address is known until every label has been
placed.
Boot in a loadable program fills the entry field rather than being
installed - vector zero is where the whole machine starts, and a program
loaded into a running system has no business saying anything about that.
A boot image is the one thing that does, so there it is installed like any
other, behind a "VEC" marker in the SPBT file.
Device is named by the port, and Device with the five reserved names are
matched without regard to case, the way mnemonics are: they are part of
the language rather than names the programmer chose. Devices have no names
of their own, so they are given one nothing can type.
TWO BUGS, both of a kind worth naming.
The first: "is this a loadable program" was written out as an OR of the two
segment bases in seven places, and the sense wanted is the opposite in most
of them. One of the seven had it backwards and put a version ONE header on a
file carrying vectors, which a loader is right to refuse. It is one flag
now, settled once and tested the same way everywhere.
The second: finding the entry to write a handler into means calling vecFind,
which reads the entry's fields out - including the handler it does not have
yet. An address resolved into VecHandler before the find was overwritten
with zero by the find itself, and the file came out with a vector pointing
at address zero: a slot that looked installed and went nowhere. The
resolved address has a variable of its own now.
Keys.asm and console.asm go on the CosmOS disk, so the whole path can be
watched rather than only tested.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
c5e4ec3455 |
M2: the native assembler builds applications
> load Asm.sbx
> run Say.asm
wrote Say.sbx: program 46, data 93, labels 7
> load Say.sbx
> run built by the machine itself
it says: built by the machine itself
The machine assembles an application and then runs what it built. Say,
greet and Files all come out byte for byte identical to the C assembler's,
and Tests/native.sh checks all three on every run alongside the boot image
M1 already covered.
WHAT IT TOOK, and it was more than #Include and #Base:
#Include The reader is a stack of readers. The current file's whole
state goes aside - buffer and all, 292 bytes - the new one
opens, and the end of it pops the old one back. A file goes in
once; including it twice does nothing, which is what lets two
libraries depend on a third. The list is forgotten between the
passes, because the second has to walk the same tree.
#Base Cursors start there, so labels hold the addresses the program
will really have. A program that says where it goes gets the
SBEX header and a .sbx name; one that says nothing gets SPBT
and .bin. A program that bases one segment and leaves the
other unbased with content in it is refused.
#Reserve Runs of zeroes, moved over in the first pass and written in
#Align the second. How many an #Align comes to depends on where the
cursor has reached, which is why both passes keep a cursor.
#Vectors Names are read and numbered, pinned where the source pins
them, so SWI osPrintString resolves. Every application needs
this - a program that calls a service names a vector declared
in a file it includes.
THE TWO PASSES ARE NOW ONE LOOP, walked twice, with Emitting the only
difference. They have to agree about the length of every token, and the way
they stop agreeing is by being two pieces of code that drifted apart -
which is the exact shape of the bug this assembler found in the C one.
Sharing the body means there is nothing to drift. What is left is checked
anyway: the second pass compares its own totals against the first's and
refuses to write the file if they differ.
THE BUG WORTH RECORDING. The tokenizer holds one character of lookahead,
and at an #Include that character belongs to the file being put aside. It
was carried across and handed back on the way out, which is wrong: a file
runs out in the middle of whatever the tokenizer happens to be doing, so
the character arrives in the middle of a word. `start:` came back as `s`
and then `tart:` - and the result assembled into a perfectly plausible
file. The fix is to undo the read instead, so the character is simply still
there when the file is opened again and the question of when to hand it
back never arises.
Three smaller ones, all old friends: three places took the CONTENTS of a
buffer where they wanted its ADDRESS; vecTakeAuto returned its answer in A,
which a RET puts back; and pass two re-declared every vector because only
the label table was being skipped on the second walk.
sameText moved down into numbers.asm from the label table - four parts want
it now, and a reader test that includes neither labels nor tokens has to
build on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
affe9d09ea |
Put the assembler and some source on the CosmOS disk
'make run-cosmos' now boots a machine with Asm.sbx on the disk and two
source files to point it at, so the thing can be watched working rather
than only tested:
> load Asm.sbx
> run hello.asm
wrote hello.bin: program 17, data 14, labels 2
> run strings.asm
wrote strings.bin: program 62, data 88, labels 4
Both come out byte for byte identical to the C assembler's, and both run.
hello.asm and testPrograms/stringKeyword.asm are the two single-file
programs with no #Include, which is what the native assembler handles so
far; strings.asm is the second because it has a subroutine and a label
used before it is defined, so it exercises a forward reference that
hello.asm does not.
The assembler builds from its own directory rather than from Apps/,
because it is not one file. Its pieces are found beside it without being
told, since an include is looked for next to the file that asked for it
before anywhere else; only services.asm needs the include path.
ALSO A CORRECTION. Asm.asm's header, its usage message and the Assembler
Manual all said 'run Asm.sbx hello.asm'. That is the convention we talked
about wanting later, not the one this shell has: load and run are separate
commands, so the whole rest of the run line is the argument and that form
asks for a file called "Asm.sbx hello.asm". All three now say load first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
dcb331c151 |
SplitBit assembles SplitBit: M1, a single file with no includes
Programs/CosmOS/Assembler/ is an assembler written in SplitBit assembly. It
runs under CosmOS, reads source off a SplitBit disk, and writes a binary back
to it with no host involved anywhere:
> run Asm.sbx hello.asm
wrote hello.bin: program 17, data 14, labels 2
THE ACCEPTANCE TEST IS THE BYTES. Tests/native.sh assembles Programs/hello.asm
both ways and compares the two files byte for byte, then runs the one the
machine built. "It ran" and "the sizes look right" both pass for a binary with
a label one byte out, which is a program that jumps into the middle of an
instruction - so the only honest test is the one SplitDisk and sbfs.asm
already work under: two implementations of one written specification, each
checking the other. The files are identical and the result prints Hello,
World! in 70 cycles.
hello.asm is the target because it is the oldest program in the repository.
The first thing this machine ever ran is now the first thing it assembles for
itself.
TWO PASSES OVER STREAMED SOURCE. The C assembler reads every token of every
file into one array; that cannot port, because cosmos.asm alone is 56,047
bytes against 64K of Data Memory. The native one streams through a 256 byte
window, twice, and keeps only the label table between the passes. Two passes
suffice because every length is known without resolving anything - an
instruction's from its shape, a value's is one, a string's is its characters
and a zero - so the first pass fixes every address and the second never needs
a fixup list. A forward reference stops being a special case and becomes the
reason there are two passes at all.
The parts, each checked before anything was built on it:
source.asm characters out of a file of any size, with a line number
token.asm tokens out of characters, one character of lookahead
classify.asm what a token is, in the C assembler's order, which IS the
language: keyword, instruction, value, string, label
labels.asm names packed in an arena, four bytes of index each
numbers.asm sixteen bit arithmetic, since sbfs.asm's cannot be reached
table.asm the instruction set, generated by the same script the
monitor's copy is, and now BOTH are checked by docs.sh
readTest.asm and tokenTest.asm check the reader and the tokenizer on their
own, recorded as cosmosSource and cosmosTokens. A wrong classification does
not produce a wrong byte somewhere obvious; it produces a right looking
program of the wrong length, so it is worth catching where it happens.
WHAT IT REFUSES: #Include, #Base, #Align, #Reserve and #Vectors are refused
by name rather than ignored. Skipping a directive would produce a file that
looked right and was the wrong length, which is the worst thing an assembler
can do.
Two traps worth recording, both already known to this project and both hit
again: CALL restores A, B and DP0-DP2, so three routines returning an answer
in A had it undone by their own return; and numStep works on DP0, so three
sites that set DP1 left a pointer that never advanced.
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> |
||
|
|
b36d438132 |
Add the CosmOS README, brought up to date with today's changes
Written by Anachronaut and not previously committed. Three things in it had
gone stale between writing and now, and are corrected here rather than
committed knowingly wrong:
- the shell command table still described dump, which the monitor
supersedes, so it now describes monitor mode and its commands
- the service table listed four services, and there are nine
- the application list did not mention Files
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3e87a67809 |
Edit: use the system's services instead of carrying the filesystem
The editor included sbfs.asm and console.asm, which was 3235 of its 4941 bytes. Both come out: reading and writing a file, printing and reading a line are services now, and sbfsMount goes entirely because the system has already mounted the disk. Only text.asm remains, at 357 bytes, because textSame and textNumber own nothing and touch no device - sharing those is a linker's job rather than an operating system's. 4941 bytes to 1983, a little better than predicted because dropping the mount and the blocks-and-tail arithmetic went with the library rather than being translated. ConsoleEndOfInput went with console.asm, so the end of input is now read from the console status port. That is better than what it replaced: the port is there whether or not the system is. Nothing it does changed, and that is checked rather than asserted. The recorded output of the cosmosEdit test does not move by a single byte across this commit - a test written days before the rewrite, checking a property rather than an output, and therefore able to verify a change it knew nothing about. One thing is lost and worth naming: there is no way to ask whether a disk is present, so the editor's "there is no disk" message is gone. On a diskless machine it now opens an empty document and fails at w instead. The shell already refuses to load without a disk, so the editor cannot normally be reached that way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0b6d2be43f |
CosmOS: a service interface for the disk and console, and the monitor in the shell
Two changes that arrived together because both live in cosmos.asm. THE SERVICES. A loaded program that wanted a file had to include the whole filesystem, carrying two and a half kilobytes of a private copy of code the system already had running, and then mount a disk that was already mounted. Five services are added at pinned numbers 20 to 24: osFileRead, osFileSave, osFileDelete, osFileRename and osPrintNumber. The sizes fit the registers exactly in both directions. 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 whose shape both sides must agree on. There is deliberately no service to mount a disk. The system mounts one before its first prompt, and a program mounting it again was only ever a consequence of owning a second copy of the library, so that call disappears rather than moving. Apps/Files.asm writes, reads, renames and deletes a file in 645 bytes and includes nothing but the service names. THE MONITOR. Previously an application, now part of the shell, because an application occupies the one region a loaded application is given: a monitor that was an application could never examine another one, since loading the thing to be inspected would replace the thing doing the inspecting. "monitor" turns it on and the prompt becomes "*". It is a mode rather than a sub-prompt, and it persists: because the mode is a variable the prompt reads rather than a second loop, and every path back to the prompt goes through one place including osExit, a program started with "g" that gives the machine back arrives at the monitor prompt it was started from. Examining a program and running it therefore do not interrupt each other. "exit" leaves whatever you are in. It supersedes dump, and adds disassembly, writing bytes, and jumping to an address. Its instruction table is generated from the assembler's own list by Tests/instructiontable.py rather than typed again, and Tests/docs.sh checks both that the system's copy matches the generator and that the lengths that table implies are the ones the manual's Bytes column prints. A disassembler that disagreed about a length would not print one line wrong, it would lose its place and print everything after it wrong. Also here: b refuses a bank that is not registered, since asking the controller for one is refused and a refusal nobody catches stops the machine; g records the Stack the way run does, without which a program returning through osExit restored whatever the last run had left; and make cosmos-disk now depends on the system as well as the image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
301716e869 |
sbfs: give sbfsMatchEntry a counter of its own
sbfsFind counts a block's eight entries in SbfsCount and calls sbfsMatchEntry for each one, and sbfsMatchEntry used the same variable for its own twenty two byte comparison. A name differing at its first byte returned without decrementing, leaving 22 in the entry counter, so the search then walked twenty two entries through a buffer holding eight: off the end of it, into whatever data happened to follow, matching against rubbish and reporting a file that is not there as found. It had always been there. What decided whether it looked like it worked was the bytes after SbfsBuffer, so it stayed hidden until unrelated data was added and changed them. It showed up as "dir" followed by any failed "load" reporting that the file could not be read rather than that it does not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c4b59acc68 | Infrastructure for system services through software interrupts. | ||
|
|
e3100b4718 | Fixed assembler bug that caused crash on IR array resize. Added line editor app. | ||
|
|
1d1a14318c | Programs can now list and share vectors. |