Commit Graph
15 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 2ad8edf9bc A program can see a directory
Every file service took a name a program already knew - read it, save it,
rename it, delete it, ask how big it is - and none of them could find out
what names there are. dir could list only because it lives in the shell
and calls the filesystem directly. So a file manager, a backup, and the
package manager still to come were each unwritable for want of this.

osDirFirst and osDirNext. DP0 says where to put the name and B how much
room, the same bargain osArgument and osWhereAmI offer.

Q ANSWERS THE KIND rather than a yes or no, so one value says both
whether there is an entry and what it is: 0 a file, 1 a directory, 2 a
save that stopped before it committed, 0xFF nothing more. A caller that
only wants names tests for 0xFF and ignores the rest.

The size is deliberately not in it. A walk hands back a name, and a
program that wants the size asks osFileInfo about that name - the
alternative being a record in memory whose shape both sides have to agree
on, which services.asm went out of its way to avoid for file sizes.

Walk.asm is the first program that can see a directory, and it asks
osFileInfo about each entry BETWEEN two steps of the walk. That is the
hazard rather than decoration: where a walk has got to and where the last
file asked about lives are both held by the system, and two things
sharing one position would show as a listing that stopped early or said a
name twice. Then the same walk in /Apps, since one that only ever ran at
the root would not have proved it walks where you are.

A directory is not asked about at all. osFileInfo answers for one
perfectly well and says nought blocks, which is true and reads as a size
- and nought is a size a file can genuinely have.

The lint baseline moves by one. The kind is decided by a chain of bit
tests in the shape dir already uses five hundred lines away, and arms of
a comparison chain each loading the same variable are the case this
project's own rule says not to collapse: the repetition is what lets a
new arm be dropped in anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 17:55:41 -04:00
AnachronautandClaude Opus 5 2defbb49e2 A program can ask where it came from
SWI osWhereAmI hands back the path the program was loaded from, on the
same terms as osArgument, and Libraries/path.asm joins a name to the
place another thing is in. Between them an application can find its own
assets: ask where you are, then pathBeside that and the file's name.

The answer FOLLOWS THE PROGRAM AND NOT THE PERSON, which is the whole
point and the reason the working directory could not serve. A program's
assets are relative to the program and its arguments are relative to
whoever ran it, and cwd can only be one of them - setting it to the
program's own would mean "Play mytune.tune", typed by somebody in their
own directory, looked in Play's.

It is made absolute before the program starts, because the path the
search settled on may be a bare name: a program found where somebody was
standing is named by the word that was typed, and a bare name means the
working directory - which a program is entitled to move out of. Worked
out once, at the start, since where a program came from is a fact about
its start and cannot change afterwards.

Joining is a LIBRARY and not a service. A service that opened a file
relative to the program would need a twin for every file operation there
is - read, save, info, block, start, write, done, delete, rename - while
one service handing back a path composes with all of them.

---- And the root's own path was "//" ----

Found by the first caller that asks. shellPath prepends a separator in
front of whatever string it is given, so being handed the separator
itself wrote two of them. Nothing saw it while the only caller was the
prompt, which asks where it is only when that is not the root. It is
handed an empty string now, and cosmosWhere runs a program from the root.

Where.sbx exists to be run rather than read, and is on the test disk
twice: at the root, where it is found by the bare word typed, and in
/Apps, where it is found by a path that already says where it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 15:27:57 -04:00
Anachronaut 418631a221 The orbit check is back, and osFileRead says it reads in blocks
Placing a state retired the reason the orbit check was deleted. Reaching
a given orbit through the controls takes a sustained burn while holding
height, and the phase of that burn against the gravity tick - one frame
in ten - decides whether the thruster is seen at all, so two pad files a
frame apart fly differently. The old check passed against one disk and
failed against another, which is a check measuring the boot time rather
than the physics.

Placed at eighty sideways it climbs to row 51, falls to row 190, and
climbs again to row 20 - and the turning points are BROAD, tens of pixels
across, so the samples have nothing like the margin problem the old one
had. Three claims: it climbs, it turns over on its own, and it comes
round again no lower than the first time.

Both halves of the mechanic are separately caught. Without the outward
push it sinks and lands and never climbs; without the exchange it climbs
away and never comes back, which is the one way trip the whole thing
exists to prevent.

---- And osFileRead writes whole blocks, which nothing said ----

A disk is read a block at a time, so a sixteen byte file still puts 256
bytes where it is told to. Reserving exactly the file's length writes
over whatever follows - a quiet corruption rather than a refusal, and it
looks like a bug somewhere else entirely. It cost an afternoon here: the
state buffer sat in front of the view tables, so the program read its
state, wiped the numbers every gauge draws from, and left immediately.

Said now in services.asm beside the vector and in the CosmOS manual,
along with the pattern that works: reserve the length rounded up to the
next 256, read into that, and copy the parts wanted where they are wanted.

The orbit fixture also needs its own keyboard file. The shared one holds
a key down every forty eight bytes for the held-thruster check, which
would fly this orbit as well as measure it.
2026-09-04 11:31:33 -04:00
AnachronautandClaude Opus 5 ab72443b99 Give the screen back: osTakeScreen, and the RAM disk earns its keep
A program that takes the whole screen leaves the shell a blank one, and
whatever was on it is gone. There was nowhere to put 48K of video memory on
a machine with 64K of Data Memory that CosmOS already lives in.

A DRIVE MADE OF MEMORY IS SOMEWHERE. The screen goes to a file on the
scratch drive - the first volatile drive found at boot - like any other
file, and comes back from handleExit alongside the vectors and console mode
already put back there. The filesystem does the allocating, so this
invented nothing: it is 196 pages of tiles, map and palette, with a block
on the front holding the cursor, the four scroll registers and the mode.

NOT AUTOMATIC, and that is the whole design. Saving on every program start
would be cheap enough; restoring on every exit would be wrong, because dir
and Files and Say print and stop and their output is the reason you ran
them. A program says it took the screen, and one that says nothing behaves
exactly as every program did before this existed.

It deleted thirty lines of Grid, and they were all wrong anyway: four
scroll registers put back by hand, the map filled with spaces, the cursor
sent home, palette bank 0 written out - and the other fifteen banks kept
Grid's colours, because there was nowhere to have kept the real ones. Grid
is 64 bytes smaller and gives back what was actually there.

The check compares the screen before against the screen after, CELL BY
CELL, and allows only the rows around the cursor to differ - found from
where the text ends rather than guessed at, because the first version
assumed the cursor was near the bottom of the screen and let three real
differences through.

Two things cost time and neither was the feature:

  - An edit adding "SWI osTakeScreen" to Grid was in the same script as a
    failing s.index, so the file was never written - and the COMMENT
    describing the call did land, from a later edit. Grid documented a call
    it did not make, and read as though it should have worked.
  - docs.sh caught osTakeScreen having no row in the services table, which
    is the check the service layer added for exactly this and the second
    time it has earned itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-31 18:47:44 -04:00
Anachronaut 87d819847e A program can say how it went
SWI osExit takes a status in A, and the shell keeps it. Fifty eight exits
across twenty three programs now say deliberately whether they worked: 25
did what they were asked, 24 did not, 9 were asked wrongly. Compare is the
exception and says so - one there means the files differ, which is a result
rather than a failure, the way diff has always had it.

IN A RATHER THAN Q, which is not a departure from the rule that a service
answers in Q. This one takes an ARGUMENT, the way osPrintNumber takes A and
B, and it never returns to answer anything. A is free precisely because a
return would have put it back - and Q is the ALU's output, so a small
number costs four instructions there against one in A.

The shell does not print it. A program that failed has already said so in
words and a number beside that is noise, so osLastStatus hands it back and
Status is the program that shows it. That indirection is the point: the
number exists for the thing that cannot read words.

MARKING THE EXITS FOUND A DEFECT ON THE FIRST RUN. Type and More printed
why they had failed and then fell through into the success exit, reporting
that all was well. Nobody had noticed, because while the only reader was a
person, the person could see both the complaint and the claim.

Two smaller things. Snake sets the console to line mode and then exits with
zero, and the linter flagged the second RSTA as redundant - an exit status
and a console mode, equal by accident, which is the class that must never
be collapsed. And the README still taught answering by writing into the
frame, three months of habit that SRET replaced yesterday; that section is
gone and the one describing SRET stands in its place.
2026-08-27 19:16:21 -04:00
Anachronaut ce2a2cd7e6 Settle is a program, and a machine with no fallback still starts
The boot state opened a loop that could not be closed from inside: the
machine said "settle it to try again" and gave you no way to do so. Settle
closes it, in 349 bytes.

A PROGRAM RATHER THAN A SHELL WORD. The shell is for the things that cannot
be done without it, and this is not one - it reaches the system through SWI
like anything else, which means it can be replaced, left off a disk, or
called by whatever comes to call programs in turn. That last one is the
point: a shell word is not callable by anything.

Two services for it. osBootState answers in Q, and a machine with no disk
answers settled, because there is nothing there to be unsettled about.
osBootSettle puts it back. SETTLING IS THE ONLY WRITE A PROGRAM GETS -
marking a start as trying or fallen back is the loader's business, and a
service that let a program claim either would let it lie about something
the loader has no way to check.

And a hole the tests walked into, which was mine rather than theirs. With
no fallback configured, a failed start left the machine unable to start at
all: the mark said do not use the system, and there was nothing else to
use. That turns "the last start failed" into "no start is permitted", which
is worse than the problem the mark was added to solve. With nothing to fall
back to it now tries the configuration again and says so - a failure that
was passing recovers, and one that is not leaves the machine exactly where
it would have been without any of this, which is the most that can be
promised when there is only one thing to start.

docs.sh caught both new services having no row in the services table before
anything else did.
2026-08-27 17:02:59 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 16:58:17 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 14:53:37 -04:00
AnachronautandClaude Opus 5 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
2026-08-24 22:22:45 -04:00
AnachronautandClaude Opus 5 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
2026-08-20 21:19:36 -04:00
AnachronautandClaude Opus 5 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>
2026-08-19 22:09:21 -04:00
AnachronautandClaude Opus 5 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>
2026-08-19 18:19:15 -04:00
Anachronaut e3100b4718 Fixed assembler bug that caused crash on IR array resize. Added line editor app. 2026-08-17 23:26:21 -04:00
Anachronaut 9e3425d34b Programs can now pin specific routines to specific vectors in SplitBit assembly. Added snake game. 2026-08-17 19:12:42 -04:00
Anachronaut 91c9d49d1b CosmOS pre-alpha and launchable application versions of old programs. 2026-08-17 15:31:49 -04:00