Commit Graph
61 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 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
2026-08-21 18:16:27 -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 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>
2026-08-19 22:29:50 -04:00
AnachronautandClaude Opus 5 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>
2026-08-19 22:19:15 -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 c4b59acc68 Infrastructure for system services through software interrupts. 2026-08-19 15:16:30 -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 1d1a14318c Programs can now list and share vectors. 2026-08-17 21:39:27 -04:00
Anachronaut 08624925fe Interrupt on keypress mode 2026-08-17 16:02:44 -04:00
Anachronaut 91c9d49d1b CosmOS pre-alpha and launchable application versions of old programs. 2026-08-17 15:31:49 -04:00