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>
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>
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>
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>
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>
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>
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>
A token is classified after its quotes have been stripped, so a string
literal reading "ADD" looked exactly like the ADD instruction and was
assembled as one. It failed with "attempting to assemble outside the
Program Segment", a message about a mistake nobody had made.
The literal and label checks were already guarded against strings and the
instruction check was not. Mnemonics match without regard to case, so
"or" and "and" were caught by this too, and those are ordinary enough
words to want in a message.
Third of its family, after a string beginning with '0' being read as a
malformed number and a string in the Program Segment being silently
discarded. All three have the same root.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The loadable program format's header has been in the working tree since
the loader was written and has never been in the repository: the gitignore
rules matched Source/Assembler as a directory, so it was silently
untracked. Anything cloning this repository could not build it, because
secondPass.c includes this file.
This is the failure the previous commit describes, having already happened
once without being noticed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Without a leading slash a pattern matches at any depth, so the rules for
the three built binaries also matched directories: "Assembler" ignored
Source/Assembler. The files already in it stayed tracked, which is why
nothing had gone wrong yet, but a new source file added there would have
been silently untracked and left out of a commit.
Found by git refusing to stage a change to Source/Assembler/firstPass.c.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>