Compare commits

..
58 Commits
Author SHA1 Message Date
Anachronaut 79727044b7 Reboot, and the machine device that makes it possible
Until now the only way to restart was to stop the emulator and run it
again, which meant the one thing the machine could not do was the thing
Once was written for. The loop now closes without leaving it:

  > Once /System/Boot/bare.bin
  next start: /System/Boot/bare.bin, once
  > Reboot
  starting again
  stage two
  just this once: /System/Boot/bare.bin
  bare metal: no system, just this

Writing 1 to port 0x13 asks the machine to start over. A PORT RATHER THAN A
SERVICE, because a reset has to work when the system does not: something
only askable through SWI would be unavailable in exactly the case that
wants it most, and a program that owns the whole machine has no system to
ask. It is device class 0x04, in the range kept for the machine rather than
among the peripherals, because it is not one - it is not attached to
anything and cannot be unplugged.

WHAT A RESET REPEATS IS HOW THE MACHINE STARTED. Named an image, the
emulator places it again; named none, the ROM is shadowed again and reads
the disk. Anything else would mean a reset changed what the machine IS,
which is the one thing a reset must not do. Both are tested.

Taken between instructions, because a device cannot restart the machine
from inside the instruction that asked: the CPU is part way through a step
and its state is not yet anything a reset could leave behind consistently.

The disk stays attached and keeps everything written to it - that is what
warm means. The vector table is cleared, which is the one deliberate
departure from leaving memory alone: a vector points into whatever
installed it, and after a reset that program is not running, so a handler
left behind would aim an interrupt at an address belonging to something
gone. It is the argument CosmOS already makes at exit, applied to the
machine.

Reboot is 45 bytes, most of them the word it prints.
2026-08-27 20:56:46 -04:00
Anachronaut 7b28f48f52 Once: start something else on the next start, and only that one
A program that owns the whole machine had nowhere to run. It cannot be
started from the shell, because starting it means there is no shell, and
pointing boot.cfg at it means a machine that keeps starting it - which is a
poor place to find a mistake in something written five minutes ago.

Once writes /System/Boot/once.cfg, in the same format as boot.cfg and read
with the same routines, because a second format for one setting would be a
second format. The loader reads it before boot.cfg and DELETES IT BEFORE IT
JUMPS, which is the only moment there is: after the jump the loader does
not exist.

Consumed by being read rather than by working, so a one shot that hangs
cannot hang twice - the request is gone before the image ran, and the next
start reads boot.cfg like any other.

THE BOOT STATE IS NOT TOUCHED, and the first version got that wrong. It
marked the start the way any other start is marked, and then every
successful bare metal boot reported that it had never arrived - because a
program with the whole machine has no filesystem to clear a mark with and
is doing nothing wrong by not having one. Found by running it: the image
printed its line and the next start still said the last one did not.

Three disks, each a start further along, so none of the tests depends on
another having run.

The loop is closed on the machine now: write it in Edit, assemble it with
Asm, ask for it with Once, restart, watch it own the machine, and the
system comes back without being asked.
2026-08-27 20:02:42 -04:00
Anachronaut 89c667848b Edit read a file into a buffer it never checked the size of
Opening hello.asm showed a thirty one line file as three, one of them cut
short. Opening it again hung the machine: the emulator kept running and
nothing ever answered.

Entry is the buffer a line is read into, and it is followed in memory by
TextHead and ArenaFree - the head of the document, and the pointer its line
allocator hands out. The loop that splits a file into lines copied
characters in WITH NO BOUND AT ALL, so a 94 character line wrote thirteen
bytes over both of them. The list head then pointed into the middle of the
text and the allocator handed out an address inside the file, which is why
the second open walked a list that led back into itself for ever.

Typing was always safe. osReadLine is told how much room there is, so a new
document behaved perfectly and a source file did not - which is exactly how
the user found it, and why it looked like a mystery rather than a bug.

The bound is there now, and the buffer is 128 characters: what a line is
everywhere else on this machine, the same number configuration files use,
rather than a second answer to a question already answered. hello.asm fits.

A file with a longer line is REFUSED rather than shortened. This is an
editor - a line cut on the way in would be written back cut, and the file
damaged by having been looked at. It says so and exits with a status of
one, which it can do since this afternoon; the file is byte identical
afterwards, and the test checks that.

Opened twice in the test, because once is not enough to see it: the first
open does the damage and the second is what never returns.

This is the third time this shape has turned up: a buffer written past its
end into the variables that happened to follow it. The prompt walked off
CwdText into the shell's own command names; the assembler's output ran into
its label table. Every one was found by a person using the machine.
2026-08-27 19:41:51 -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 b4206673a6 The clean install disk is one the machine can start itself from
make run-cosmos handed the emulator a boot image AND a disk, so it took the
direct path every time: memory placed from outside, nothing on the disk
consulted about it. The whole boot chain was built and tested and then not
used by the one command that runs the system.

The disk is formatted with a boot area now, stage two goes into slot zero
as raw bytes, and the system is an ordinary file at /System/Boot/cosmos.bin
- which is what boot.cfg would choose between if there were one. There is
not: stage two falls back to that name when the file is missing, and a
clean install with nothing to configure is the right default.

run-cosmos names no image, so the machine starts itself. The old behaviour
is run-cosmos-direct, which is worth keeping and worth naming honestly: it
is what a debugger does, and it is what to use when the thing being
debugged is the boot chain, since it skips the boot chain.

/Source gains stage1.asm and stage2.asm, so the machine can rebuild what
starts it. Everything stage two includes is already in /Lib, so Asm
stage2.asm on the machine produces the bytes a boot slot takes. Stage one
is the exception and always will be: it is the ROM, and the one part of
this that a disk cannot replace.

Second slot left empty, which is what makes replacing the first survivable.
2026-08-27 18:45:52 -04:00
Anachronaut f1cc2e56b2 The last handler that knew a frame offset
handleReadLine used DP1 for the frame and the conversion only looked for
DP2, so it kept the whole dance - and the dance had become a no-op that
looked like work: MVQA copied Q into A, A was written over the saved Q, and
RETI restored it, which is where it started. readLine leaves the length in
Q and SRET keeps Q, so the answer simply stands.

No handler in CosmOS knows what an interrupt frame looks like now. The
0d02 and 0d05 offsets still in the file are structures - an SBEX header, an
entry in the instruction table - and not frames.
2026-08-27 18:30:28 -04:00
Anachronaut 61a80ae13d Put the manual's sections back in opcode order
The user's edit. Moving branches to 0x60 and subroutines to 0x70 left their
sections sitting where they used to be numerically, between the ALU and the
registers, so the manual read in an order the opcode map no longer did.

The list has always been arranged by opcode sequence rather than by
importance or by how often a thing is used - arbitrary, and now canon.
2026-08-27 18:27:46 -04:00
Anachronaut c8c9f0b363 SRET: a handler answers the way a subroutine does
CALL saves A, B and Data Pointers 0 to 2 and nothing else, which is exactly
why Q and DP3 are how a subroutine hands something back. An interrupt saves
all of it, so a service with an answer had to reach into its own frame and
un-save two fields by hand:

  MVSD.2
  DPUP.2 0d02           ; the saved Q, by an offset it had to know
  STA.2
  RETI

Thirty places in CosmOS did that. Every one knew the frame's layout by
heart, and all thirty would have gone quietly wrong the day the frame
gained a field - the same duplicated fact this project keeps being bitten
by, except duplicated into thirty places AND into the CPU.

SRET is 0x76, in the seat the block split left for it. It is RETI's frame
with RET's rule applied: A, B and DP0 to DP2 come back, the saved Q and DP3
are dropped, and the Interrupt Flag is restored from the frame - only that
bit, so carry survives a service the way it survives a call, and there is
one rule rather than two. RETI stays exactly as it was: a hardware handler
has nothing to say and must leave no trace.

CosmOS is 10,969 bytes against 11,122, and no handler knows a frame offset.

TWO MISTAKES WORTH RECORDING, both mine, both caught by tests.

The first conversion matched STA.2 with a regular expression that did not
allow a trailing comment, so it ran past the end of one handler and into
the next. The second understood the pattern and still got it wrong: the old
frame write carried the answer from A into the saved Q slot, so simply
deleting the write left Q holding whatever it happened to hold. Services
that answer by calling something were fine - Q already had it - and
services that set A directly silently reported success for every failure.
cosmosCwd is what noticed, by saying "cannot go there" about a directory
that was there. Sixteen handlers move the answer into Q now.

Seven MVQA went with it. They copied Q into A so the frame write could
carry it; SRET puts A back, so they moved a value nobody would ever read.
2026-08-27 18:18:36 -04:00
Anachronaut cd5f548736 Move the opcode map: nothing in 0x0X, and room for a return variant
Three blocks move and nothing else changes. Branches take 0x60, subroutines
take 0x70, and the ALU moves up into the 0x10 block the two of them used to
share. Order within each block is preserved exactly - this relocates them,
it does not rethink them.

WHAT IT BUYS IS AN EMPTY 0x00 TO 0x0F. Program Memory that was never
written, or a load that stopped part way and left zeroes in its tail, used
to read as a long run of ADDs: the machine carried on through them, arrived
somewhere unpredictable, and whatever broke there was a long way from the
byte that caused it. Now it faults where it is met:

  Fault: 0x00 at Program Address 0x0004 is not an instruction.

That is the address of the byte after the last real instruction, which is
the difference between a diagnosis and a search. Reserving the whole nibble
rather than just 0x00 means a run into blank memory faults wherever it
starts rather than only when it lands on the right byte. runOffTest records
it, and the block is left empty for whatever turns out to want it.

The other half is room: branches and subroutines had filled 0x10 to 0x1F
between them, so a service return that keeps Q and DP3 had nowhere to sit
next to its family. It has 0x76 waiting now.

Five places wrote an opcode down that the scripted remap did not reach, and
four of them were found by tests rather than by looking:

- secondPass.c lists which opcodes take an address, and firstPass.c knows
  SWI by number. Missing those made XOR read as a branch.
- Asm.asm knows SWI by number too, being the other assembler. Missing it
  made the native and host assemblers disagree byte for byte, which is
  exactly the check that exists to catch a thing known in two places.
- loaderTest.asm carries a hand written payload, and its RETI was 0x19. To
  the assembler those are numbers and to the program they are data, so
  nothing but running it could notice. It says so in a comment now.
- The Assembler Manual prints the bytes hello.asm assembles to, and two of
  them were branches.

The monitor's recorded disassembly moved by exactly the bytes it should:
18 became 72 wherever SWI appears, with SETD and INIB untouched and every
disassembled line still reading the same.
2026-08-27 18:05:54 -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
Anachronaut dc74149321 B4: the disk remembers whether the last start arrived
The loader marks the superblock before it hands over and the system clears
the mark when it reaches its prompt, so a system that crashes on the way
there leaves it set. The loader finding it still set next time is how a
machine that will not start says so to the only thing in a position to do
anything about it. Without that, pointing boot.cfg at something that dies
before the shell is a machine that can never be told anything again - the
shell is the only way to change the file, and the file is what stops the
shell from starting.

Three states rather than two, and the third is the one worth having:

  0 settled    the last start arrived; use the configuration
  1 trying     handed over, and nothing came back to say it got there
  2 fell back  a try failed and the fallback was used, until settled

With only 0 and 1 the machine alternates for ever: fall back, reach a
prompt, clear the mark, retry the broken system, crash, fall back. State 2
stops that. A system known not to start is not tried again until somebody
says the situation has changed.

REACHING THE PROMPT IS A DELIBERATE THRESHOLD. It is not a claim that the
system works - a shell can be reached by something broken in every other
way. It is the point where a person can type, which is exactly what the
fallback exists to give back: anything wrong past there is fixable from the
prompt and nothing wrong before it is fixable at all.

The routines live in sbfs.asm because both the loader and the system read
and write this byte, and two pieces of code with their own idea of where a
byte lives is what this format has two implementations and a byte for byte
comparison to avoid.

And the trap this system documents in its own manual caught me anyway: the
first version handed the state back in A, which CALL restores, so every
read got whatever the caller happened to be holding. It comes back in
memory now, and the comment says why.

Three disks differing only in the state on them, so the tests read as three
consecutive starts of one machine while none depends on another running.
2026-08-27 16:51:39 -04:00
Anachronaut 546f336823 Configuration files, and boot.cfg as the first of them
One setting to a line: a key, a space, the rest of the line is the value.
A semicolon starts a comment. The format was noticed rather than designed -
textSplit already cuts the first word off a line and leaves the rest, and
textSame already insists two strings end together, so reading a setting is
those two routines and a loop. It is also what the shell reads, which makes
a configuration line a command line the machine reads instead of a person
typing one.

The format was chosen by asking what the BOOT LOADER could manage, because
it is the worst case in every direction: a few kilobytes, no operating
system to report to, and if it fails the machine does not start. Two
formats would be worse than one and the loader cannot have the richer one.

CONFIGURATION IS ADVICE. A missing file, a missing key, an unusable value,
a line too long to read: all of them mean use the default and none is a
failure. BUT QUIET IS NOT SILENT - a setting somebody meant, which did not
take effect, says so. That was the user's addition and it is the better
rule: the default alone leaves the only symptom being that the machine did
not do what somebody asked.

So two routines. cfgGet reads and says nothing, because reading three
settings should not report one bad line three times. cfgCheck reads the
file once and reports, and is handed the caller's list of keys - whether a
key means anything is the only part of this a shared reader cannot judge.

/System/Boot/ holds the boot files, and stage two reads boot.cfg for what
to start, with a fallback to try if it does not work and a name compiled in
for when the file says nothing.

THE TEST FOUND A REAL BUG, and it is the interaction I would not have
thought to look for. First-match-wins met an empty value: a file with

  system
  system /System/Boot/bare.bin

matched the first line, handed back nothing, and the machine tried to start
a file with no name while a good setting sat underneath. An unusable value
is an absent one - which is what "configuration is advice" says, and this
is where it earns its keep.

cfgBare starts an image with no operating system in it at all, which is
what loading an ordinary boot image buys: a program wanting the whole
machine is a file like any other, chosen the same way the system is. Three
disks differing ONLY in boot.cfg, so each is a test of the file rather than
of the machinery under it.
2026-08-27 16:04:21 -04:00
Anachronaut 54ff7196c9 Stage 0: the emulator carries the ROM, so a disk is enough
./SplitBit --disk system.img
  stage two
  CosmOS
  >

No boot image named. The emulator shadows its built in stage one into
Program Memory - boot vector included - and the CPU then does exactly what
it has always done: reads the boot vector and starts where it points.
NOTHING ABOUT THE CPU CHANGED to make a machine that starts itself, which
is what picking shadowing over a mapped ROM bought.

The ROM is generated from Programs/Boot/stage1.asm by the makefile rather
than committed beside it, because a copy of a program kept next to the
program is a copy that goes stale. That makes the assembler a real
dependency of the emulator, which it always sort of was and now says so.
od and awk rather than xxd, which is not everywhere, or python, which the
README does not ask anybody to install in order to build this.

loadROM is loadFile given bytes instead of a path: both go through one
reader over an fmemopen stream, because a ROM is a boot image and there is
no reason for the machine to have two ways of understanding one.

Naming an image still works and is what every other test here does. That
path is not a shortcut to apologise for - placing memory from outside is a
real thing real machines allow, and it is a debugger. The help says so now.
No image and no disk is the one case with nothing to run, and it says that
rather than printing a usage message about a missing file.

run.sh gained a "rom" mode which hands the emulator a disk and nothing
else. The source column still names stage1.asm, because that is what is IN
the ROM: assembling it there says the thing the emulator carries is a thing
that still assembles.
2026-08-27 14:41:33 -04:00
Anachronaut c312853f8e The machine starts itself
stage two
  CosmOS
  > saved it
  read it back, 22 bytes:

Stage one hands over to stage two out of a boot slot; stage two mounts the
filesystem, finds /System/cosmos.bin, takes the image apart and places its
code, its data and its vector table, and jumps to the entry point the
vectors named. Nothing placed memory for it.

What it loads is an ORDINARY BOOT IMAGE, the same SPBT file the emulator
has always been handed. That was the user's call and it is the whole trick:
a second stage that loads the machine's normal image format is not a
boot-specific mechanism, so bare metal SplitBit stops being a special case.
A program wanting no operating system under it is just an image, written
under CosmOS like any other, and startable because it is a file.

Three things in it worth knowing:

- THE ENTRY POINT IS CAUGHT ON ITS WAY PAST. Program Memory cannot be read
  back, so the boot vector cannot be looked up after being installed; the
  vector loop notices the one addressed at 0xFC00 and keeps it.
- A missing "VEC" is not a fault. An image written before vectors existed
  simply ends after its data, and then the entry point is zero, which is
  what every such image has always relied on.
- Feature flags that are set mean an image asking for a machine this may
  not be, and the honest answer to a request that cannot be understood is
  to refuse rather than to run it anyway.

The test records that the system WORKS afterwards rather than that it
started. A loaded program running is what says the vector table arrived,
because a program reaches the system through SWI and nothing else; the file
written and the directory entered say the filesystem and the console came
up with it. A second disk has a boot slot and nothing to start, and says so
rather than jumping somewhere.
2026-08-27 14:13:23 -04:00
Anachronaut 82adeeb193 A boot payload can arrange its own Data Segment
Stage one places Program Memory and nothing else, because knowing where a
payload's data ends and its code begins would mean knowing a format, and
knowing formats is what ROM must do as little of as possible. But the real
second stage needs a Data Segment: sbfs.asm has variables and a string it
compares against.

The answer needs nothing new. A loadable image is written into the slot as
code followed by data, so the data image is already in Program Memory just
past the code - and the payload's first instructions blit it down to where
it was assembled for. Proved by slotData.asm, which prints from a string it
placed itself.

The padding is the part worth recording. The blit needs a length and the
assembler will not work out the difference between two labels, so the
segment is padded to a round number and that number is what gets copied.
The first draft padded to 257 and copied 256, and the byte that did not
arrive was padding, so it worked by luck. It is exact now and says why.

This is the shape the user asked for and it goes further than the
mechanism: the second stage becomes a loader for the machine's ORDINARY
image format rather than for anything boot-specific, so bare metal SplitBit
stops being a special case. A program that wants no operating system is
just an image, developed under CosmOS like any other, and selectable at
boot because it is a file.
2026-08-27 13:45:11 -04:00
Anachronaut d07b23f90b Rung 2: the machine starts itself off a disk
Stage one exists and works. It is 330 bytes of program and everything it
knows is a thing that will be true forever: which port the disk is on, that
a SplitBit disk begins with its own name, and where two numbers sit in that
first block. Not what a file is, not what a directory is, not that SBFS has
versions. It reads the live boot slot into Program Memory, jumps to the
first byte, and prints one character and halts if there is nothing there.

It is an ordinary boot image for now, so the whole chain runs on machinery
that already exists and the emulator has not been touched. Nothing about it
changes when it moves into ROM except who puts it in memory.

SplitDisk gained "boot" to write a slot and "bootslot" to choose one, kept
apart on purpose: writing a slot and starting from it are different
decisions, and joining them would make every write a commitment. A slot is
always written WHOLE, because one still holding the tail of what was there
before is one whose contents depend on its history, and stage one reads all
of it without knowing where the file stopped.

Three recorded tests, and the pair is the point: two disks differing only
in which slot the superblock names, with payloads that say different
things. One prints "booted" and the other does not, so this is a test of
CHOOSING a slot rather than a test that some bytes were read. The third
boots a disk with no boot area and gets the one character a ROM has room
for. Eight more host checks, including that a slot is padded whole.

Two things worth recording. The first draft used #Align to put the scratch
buffer at 0x8000 and produced a 33K file - thirty two kilobytes of zeroes
in something meant to be a ROM. It is an address, not storage, which is
exactly what the assembler's own scratch map exists to say.

And SplitLint caught the second in code written an hour after the baseline
that catches it. In the blit set-up, RSTA writes a source address of zero
and then RSTA writes a bank number of zero - two unrelated quantities that
are equal by accident, in the most safety critical file in the repository.
It is marked with a reason rather than removed.
2026-08-26 23:31:04 -04:00
Anachronaut 612bd1b97c B1: a boot area on the disk, reserved by arithmetic that was already there
The first rung of booting from disk. A boot area is blocks between the
superblock and the directory that the filesystem never allocates and never
sees, and NOTHING WAS ADDED TO RESERVE THEM: both implementations work out
the first usable block as directoryStart + directoryBlocks, and
directoryStart has always been a field rather than a constant. Formatting
with the directory moved up reserves everything below it. Neither allocator
changed, on either side.

Two new superblock fields in bytes that were reserved: bootBlocks at 14,
per slot, and bootSlot at 16. A disk made before this has zero in both,
which reads as "no boot area" - true, and the same shape as the version two
parent field, where the value an older disk already held was the right
answer without conversion.

TWO SLOTS, ALWAYS. A boot slot is raw blocks with no entry to rename, so
the write-a-temporary-and-rename ordering that protects every file cannot
protect it, and a machine interrupted while updating its only slot would
not boot at all - the one failure on this disk with no way back. Writing
the slot that is not live and then moving one byte makes that a machine
that boots what it had before.

bootBlocks and directoryStart say the same thing from two sides, so a disk
where they disagree is refused rather than guessed at, as is one naming a
slot that does not exist.

Checked where it matters: the HOST formats a disk with a boot area and the
MACHINE fills it, then the reserved blocks are compared against zero. The
machine's allocator is the one that had no idea any of this was happening,
which is what makes that the check worth having. Six host checks besides,
including both halves of the superblock disagreeing.
2026-08-26 22:58:45 -04:00
Anachronaut 0a2965bc63 Name the rules, say when there is nothing to say, and hold a baseline
Four things SplitLint wanted, and they build on each other.

EVERY WARNING NAMES ITS RULE, in brackets at the end the way a compiler
names the flag that produced it. Twelve rules, listed by --help. That makes
the other three possible: suppressions can name one rule and leave the line
honest about the others, the harness can assert on a rule's identity rather
than on the wording of its message, and --machine can print one tab
separated line per warning - file, line, rule, message, help - so nothing
downstream reads prose. This file's own output was parsed with regular
expressions three times in one day before it had a shape to rely on.

A CLEAN RUN SAYS SO:

  No style warnings: 121 files checked against 12 rules.

It used to exit in silence, which does not tell you it found nothing - it
tells you nothing at all, and from outside the two are identical.

A MARKER THAT SILENCES NOTHING IS ITSELF REPORTED, as dead-suppression. An
exception that outlived whatever made it necessary is the thing the
required reason exists to prevent, and naming the wrong rule now gets you
both the warning you meant to silence and a note that your suppression is
doing nothing.

AND THE CORPUS IS HELD TO A BASELINE. Sixty one warnings are left in it
deliberately and nothing stopped a sixty second. Tests/lint-baseline.txt
records how many of each rule each file should produce, so a new one fails
make test while the sixty one stay quiet; confirmed by adding an INIA 0d0
to Say.asm and watching it name the file, the rule and the count. It counts
per file and rule rather than recording line numbers, because line numbers
would churn the whole baseline whenever anything was inserted above a
warning - the same reason cycle counts are stripped from recorded output
here. ./Tests/lint.sh --bless records it again.

One thing to know for next time: the rule name was inserted before the line
number at all twenty one call sites, and the signature was changed to match
rather than the twenty one call sites being fixed. (path, rule, line) reads
no worse than (path, line, rule) and one edit has fewer ways to go wrong
than twenty one.
2026-08-26 21:00:21 -04:00
Anachronaut 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.
2026-08-26 18:02:35 -04:00
Anachronaut 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.
2026-08-26 17:53:10 -04:00
Anachronaut c146d98588 Stop the linter recommending a change that a faster helper would break
SplitLint knew that CALL restores A, B and Data Pointers 0 to 2, so a
pointer set before a call is still set after it. That is true, and it made
the tool give advice that was correct today and unsafe to take.

Of the 178 redundant SETDs it found across the corpus, 122 were redundant
ONLY because of that restore - the shape is everywhere, because it is how a
helper is given its arguments:

  SETD.0 SbfsBlock
  SETD.2 SbfsFileStart
  CALL sbfsSetWord
  SETD.0 SbfsBlock        <- flagged

Removing that last line is right until sbfsSetWord is reached with RCAL,
which restores nothing - and that is not hypothetical, it is what RCAL was
added to this machine for, measured at close to halving the assembler's
memory traffic. The failure would also be silent from the linter's side: it
forgets everything across an RCAL, so it would stop reporting while the
removals stayed removed.

So a claim now ends at any call, for pointers and for registers, the way a
claim about carry already did. 257 warnings become 127, and the redundant
SETDs 178 become 54 - which is exactly the number an independent count of
"no CALL in between" had arrived at separately.

The fixture gained a SETD and an INIA repeated across a CALL, which must
stay quiet, and the harness fails with the old behaviour put back. Two
mistakes worth recording: the new expectations first pointed at the LABEL
above the repeats rather than the repeats, which passes for free because
nothing ever warns about a label; and the block landed in the middle of
another check's comment, leaving that comment describing the code below it
instead of its own.
2026-08-26 17:42:56 -04:00
Anachronaut 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.
2026-08-26 17:32:22 -04:00
Anachronaut 2b079324ae Add SplitLint, and let it be told when something is deliberate
SplitLint reports valid assembly that has a shorter direct expression:
zero loads that could be RSTA or RSTB, Q moved through the stack where MVQA
would do, self-cancelling push and pop pairs, assignments overwritten
before use, unreachable fallthrough, one-byte pointer moves that could be
INCD or DECD, a branch to the label directly below it, a SETD reloading an
address the pointer already holds, and branches whose carry is known.

Its model is deliberately local and conservative: every label and every
directive forgets all known state, so a claim only ever lives inside a
straight-line region. It knows the calling convention - CALL forgets DP3
and keeps the rest, RCAL and SWI forget everything - and it shares
assembly.o with the assembler, so an added opcode cannot leave it holding a
private copy of the instruction table.

260 warnings across the corpus, of which three were wrong in the way that
matters: branchTest.asm and interruptFlagTest.asm exist to check that a
branch whose carry is known behaves correctly, so a diagnostic saying the
outcome is known is exactly right and exactly unwanted.

A line whose comment says "splitlint: <reason>" is now not reported on. THE
REASON IS REQUIRED and a bare marker is refused, because a suppression
nobody explained outlives whatever made it necessary. Suppressed warnings
are not counted, so --fatal-warnings does not fail on one, and the number
of them is printed at the end so the claim is visible rather than silent.

Tests/lint.sh checked a TOTAL: twenty three warnings expected, twenty three
found. That number stays right while the thing behind it goes wrong - a
rule that stopped firing while another fired twice would pass, and so would
a rule reporting at the wrong line. It now checks which warning came out
and at which line, that nothing else came out, and that the four lines
meant to stay quiet did. Confirmed by breaking one rule's message and
watching it name that rule: the old assertion passed the same sabotage,
because the warning still fired and the count never moved.

Written with the user while I was away; my part is the suppression
mechanism, the harness rewrite, and the three marks in the test programs.
2026-08-26 17:23:17 -04:00
Anachronaut 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.
2026-08-26 11:14:17 -04:00
Anachronaut 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.
2026-08-26 11:11:25 -04:00
Anachronaut 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.
2026-08-26 10:28:14 -04:00
Anachronaut c74075dc51 The review notes are all done, so the file that said to delete it goes
Six items, all fixed and all tested: the streamed commit bounded against
its reservation, the memory map checked against itself, the save guarantee
stated precisely, temporaries marked in the entry rather than by name, the
path limits measured and left alone, and the directory bounded to what the
parent field can name.

The list of design strengths at the end went with it. It was a reader
describing what the format gets right, which the README says in its own
words and at more length; keeping a second copy in a file headed 'delete
me' would only be a second copy to keep current. It is in the history if
anybody wants to read it back.
2026-08-25 23:47:12 -04:00
Anachronaut 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.
2026-08-25 23:47:02 -04:00
Anachronaut 634650cab9 The path limits are settled: leave them, and say what they are
Measured rather than adjusted. The longest path on a full install is 21
characters against a native cap of about 117, so neither that nor
SplitDisk's 512 binds anything, and both can be raised if that ever
changes. Looking for the evidence turned up the limit that did bind - the
prompt's 127 bytes, fixed in the commit before this one.
2026-08-25 23:35:40 -04:00
Anachronaut 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.
2026-08-25 23:35:33 -04:00
Anachronaut 0c240f7ad3 The temporary namespace is no longer a note
Two left: one path length both implementations agree on, and a bound on
directory entries that keeps every one of them nameable as a parent.
2026-08-25 23:22:43 -04:00
Anachronaut 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.
2026-08-25 23:22:37 -04:00
Anachronaut d6cbbb5034 Strike the three review items that are now fixed
The streamed commit is bounded, the memory map is checked against itself,
and the save guarantee says what it is. Three left: the temporary-file
namespace, one path length both implementations agree on, and a bound on
directory entries that keeps every one of them nameable as a parent.
2026-08-25 22:56:21 -04:00
Anachronaut 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.
2026-08-25 22:56:07 -04:00
Anachronaut 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.
2026-08-25 22:54:14 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 22:36:22 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 21:29:57 -04:00
AnachronautandClaude Opus 5 e0cf0a9a25 Charge the memory controller for the memory it moves
A blit cost ten cycles, which were the five port writes that set it up. The
quarter of a kilobyte that moved cost nothing, and no hardware moves a quarter
of a kilobyte for nothing.

BANKS ARE SEPARATE MEMORIES, AND THAT IS WHAT SETS THE RATE. A move between two
of them can overlap its read and its write - fetch the next byte while the last
one is stored - so it settles at a byte a cycle. A move within one bank cannot,
and costs two. A fill has nothing to read and costs one whatever the banks are.
The odd cycle on each is the pipeline filling.

That is not a modelling choice so much as a reading of the structure the machine
already has: a Program to Data blit is inherently twice the rate of a Data to
Data one, and it is legible why.

Measured: 256 bytes is 297 cycles across banks and 518 within one, both
including the instructions that ask for it.

WHAT IT TAUGHT, which was not what I expected. Charging for movement costs the
native assembler 0.4 per cent and costs directory work 13.4. The assembler reads
a block and then thinks about it for a long time, so the move is amortised into
nothing; the filesystem reads a block in order to look at it and does nothing
else in between.

So the case for a blitter that runs alongside the CPU is weaker than it sounds.
Concurrency pays when there is other work to do during the transfer, and the
place that spends its time moving memory is exactly the place with nothing else
to do - it blits a block precisely so that it can read it. What that workload
wants is a FASTER controller, not a concurrent one: a wider data path halves the
wait, and the machine is waiting either way.

Video is the case that would still want concurrency, since a frame can be moved
while the next one is worked out. That is an argument about software nobody has
written yet, and it is now an argument with numbers on the other side of it.

The byte at a time port is charged too, for the byte it moves beyond reaching
the port. Nothing polls CTRL_STATUS, so the transfer stalls whoever asked for
it, which is the conservative reading and the one the software already assumes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 21:11:47 -04:00
AnachronautandClaude Opus 5 f1e5cc46f6 A cycle is an access to memory, not an instruction
cycleCount used to tick once per instruction, so RSTA cost what SETD cost and a
CALL moving ten bytes of Stack cost what a branch cost. No machine anybody could
build works that way, and the emulator's job is to be the thing the hardware is
designed against.

Every touch of memory now goes through one of four accessors that charge for it:
fetching an opcode, fetching the bytes after it, reading or writing Data Memory,
and reaching a device port. One access, one cycle, nothing overlapped. The
accessors exist so the cost is counted where the access happens rather than in a
table of per instruction costs kept somewhere else - a table like that is a
second copy of what the code does, and the two drift.

The run loop spends a budget of cycles instead of running a count of
instructions, so the emulated rate means something: an instruction costs what it
touches, and a batch ends when the cycles are gone.

What the numbers say now: RSTA 1 and SETD 4, being one byte and four. LDA 3,
DPUA 2. CALL and RET together 24, RCAL and RRET together 8, because the first
pair moves twenty bytes of Stack and the second moves four. The average SplitBit
instruction costs 3.72 of these, measured over the native assembler assembling a
program.

And the measurement that prompted all of this: converting the filesystem's
hottest leaf routine to RCAL is 3.1 per cent cheaper on a directory heavy
workload. The old model said 0.0, which is what a model that cannot see memory
traffic must say about a change that is nothing else.

Three tests moved. settle() strips the cycle count from recorded output, so
nothing should have churned - but it was anchored to the start of a line and
replCalculator's last output has no newline on it, which leaves the halt message
mid line where the pattern never reached. Not anchored any more.

The two Life programs are bounded by a cycle count because they never end, and
that number was rescaled from 3,000,000 to 11,200,000 - the same amount of work
at 3.72 cycles to the instruction. Nothing about either program changed. No limit
reproduces the old output exactly, because the cut now lands elsewhere in a
frame, so they are recorded again rather than tuned to match.

Whether hardware overlaps a fetch with the end of the previous instruction is
left open on purpose. This is the conservative model; pipelining is a decision to
make while drawing the hardware, not one to inherit from an emulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 20:44:38 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 20:21:43 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 20:20:39 -04:00
AnachronautandClaude Opus 5 19ab36a201 The assembler can say where everything ended up
-S writes every label and the address it was given, in address order.

Nothing else knows that. A program on the disk is bytes; the monitor can
disassemble it but has no idea what any of it is called. So counting which
addresses a program calls says a great deal and names nothing - the answer
arrives as a column of numbers and somebody works out by hand which routine
each one is inside.

It was deferred when the native assembler was planned, as a listing and symbol
dump nobody needed yet. Finding out where the assembler spends its time is what
needed it: the top six call targets were addresses until this existed and are
numStep, numCompare, tokGet, srcNext, numAddByte and clsSameName with it.

Sorted by address rather than by name, because the question asked of it is
always "what is at this address".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 18:15:50 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 17:29:43 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 17:13:49 -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 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
2026-08-25 14:50:36 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 14:47:33 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 10:04:12 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 09:18:29 -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 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
2026-08-24 22:21:02 -04:00
AnachronautandClaude Opus 5 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
2026-08-24 19:08:26 -04:00
AnachronautandClaude Opus 5 78e9eef472 D1: teach SplitDisk directories, without moving a byte
SBFS version two adds directories out of space each entry had already set
aside: two of the four reserved bytes become a parent, and one of the seven
spare flag bits says an entry is a directory. The entry is still thirty two
bytes, so it still divides two hundred and fifty six and still never straddles
a block, and nothing in the block layer knows anything happened.

A directory is an entry with no blocks. That is what keeps the flat array of
entries the whole allocation map, which is the property the format is built
on: with files laid down contiguously, every block is inside some entry's
range or it is not, and an entry with no range is in nobody's way. There is
still no allocation table to consult and none to keep right.

THE PARENT IS AN INDEX PLUS ONE, so zero means the root. A version one disk has
zeroes in those bytes, and "in the root" is exactly where every file on a flat
disk is - so a version one image is already a valid version two image, with
nothing to convert and no tool to convert it with.

A disk is at the lowest version that describes what is on it. format makes a
version one disk and mkdir is what raises it, so everything built here stays
readable by a reader that has never heard of a directory right up until it
really does have one. That is what lets this land before the machine knows
anything: the whole existing suite passes untouched.

The tool gains mkdir and rmdir, and list, put, get and delete take paths. list
also now reports entries used against entries available, because a disk has two
ceilings and the entry one is the one nobody notices until it bites.

rmdir refuses a directory with anything in it, and that is not politeness:
parents are entry indices, a freed index gets handed out again, and the
children of a removed directory would reappear inside whatever took its place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-24 18:41:11 -04:00
AnachronautandClaude Opus 5 f4fb56606e Give the demo disk room for names, not just for bytes
The demo disk had eight directory blocks, which is sixty four names, and
thirty nine of them were already spoken for. Its two ceilings were nowhere
near each other: the average file on it is twenty six blocks, so sixty four
names run out with the disk forty one per cent full. Names were going to be
gone long before space was, and space is what a disk is usually short of.

Twenty four blocks is one hundred and ninety two names. The difference costs
sixteen blocks of four thousand and ninety six, three tenths of one per cent,
and the superblock has carried this number per disk since the format was
written - so nothing but the makefile line knows or cares what it is.

This is provisioning and not format. Directories, when they arrive, will not
relieve this pressure; under the settled design they add to it, because a
directory spends an entry of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-24 18:31:55 -04:00
AnachronautandClaude Opus 5 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
2026-08-24 17:25:50 -04:00
AnachronautandClaude Opus 5 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
2026-08-24 17:15:53 -04:00
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
191 changed files with 14418 additions and 938 deletions
+2
View File
@@ -7,6 +7,7 @@
/Assembler /Assembler
/SplitBit /SplitBit
/SplitDisk /SplitDisk
/SplitLint
/CLAUDE.md /CLAUDE.md
/claudeResume.sh /claudeResume.sh
/codexResume.sh /codexResume.sh
@@ -26,3 +27,4 @@ __pycache__/
# Kate leaves these beside a file it has open. # Kate leaves these beside a file it has open.
.*.kate-swp .*.kate-swp
Source/Emulator/rom.c
+36
View File
@@ -0,0 +1,36 @@
; bare.asm
; A machine with no operating system on it at all.
;
; THIS IS THE POINT OF LOADING AN ORDINARY IMAGE. The second stage does not know what
; CosmOS is; it knows what a boot image is, and this is one. So a program that wants the
; whole machine to itself - no shell, no filesystem, no services - is not a special case
; needing a special path. It is a file, written under CosmOS like any other, and started by
; naming it in /System/Boot/boot.cfg.
;
; It owns everything from address zero upward, and the only thing it can be sure of is the
; console, because that is all it asked for.
;
; Written by Anachronaut
#Program
start:
SETD.0 Said
sayLoop:
LDA.0
BRA sayDone
OUTA 0x00
INCD.0
BRI sayLoop
sayDone:
HALT
#Data
Said:
"bare metal: no system, just this
"
#Vectors
Boot start
+65
View File
@@ -0,0 +1,65 @@
; slotData.asm
; A boot slot payload that arranges its own Data Segment.
;
; Stage one places Program Memory and nothing else, because knowing where a payload's data
; ends and its code begins would mean knowing a format, and knowing formats is the thing
; ROM must do as little of as possible. So a payload that wants initialised data arranges
; it: a loadable image is written into the slot as code followed by data, which leaves the
; data image sitting in Program Memory just past the code, and the first thing the payload
; does is blit it down to where it was assembled for.
;
; This is how the real second stage will get the Data Segment that sbfs.asm needs.
;
; THE PADDING IS NOT DECORATION. The blit needs a length, the assembler will not work out
; a difference between two labels, so the segment is padded to a round number and that
; number is what gets copied. Reserve one byte too many and the last byte does not arrive:
; the first draft of this padded to 257 and copied 256.
;
; Written by Anachronaut
#Program
#Base 0xC000
start:
RSTA
OUTA 0xE0 ; SourceBank: Program Memory, where stage one put everything.
SETD.0 codeEnd
PSHD.0
POPA
POPB
OUTB 0xE1
OUTA 0xE2
INIA 0d1
OUTA 0xE3 ; DestBank: Data Memory.
SETD.0 Greeting
PSHD.0
POPA
POPB
OUTB 0xE4
OUTA 0xE5
INIA 0x01
OUTA 0xE6
RSTA
OUTA 0xE7 ; A round 256 bytes, which the segment is padded to.
INIA 0x01
OUTA 0xE8
SETD.0 Greeting
sayLoop:
LDA.0
BRA sayDone
OUTA 0x00
INCD.0
BRI sayLoop
sayDone:
HALT
codeEnd:
#Data
#Base 0x3000
Greeting:
"a payload with data of its own
"
Padding:
#Reserve 0d224
+33
View File
@@ -0,0 +1,33 @@
; slotTest.asm
; Something small to put in a boot slot, so that the chain can be proved end to end.
;
; NO DATA SEGMENT, and that is not tidiness - it is the shape of what stage one can do.
; Stage one reads raw blocks into Program Memory and jumps to the first byte. It places no
; data, because it has no way to know where a payload's data ends and its code begins
; without knowing a format, and knowing a format is the thing ROM must do as little of as
; possible. So a payload either carries no initialised data or arranges its own.
;
; Which is why this prints with INIA and OUTA rather than from a string. The real stage two
; needs a Data Segment, and how it gets one is an open question written up beside this.
;
; Written by Anachronaut
#Program
#Base 0xC000
here:
INIA 0x62 ; "b"
OUTA 0x00
INIA 0x6F ; "o"
OUTA 0x00
INIA 0x6F ; splitlint[redundant-assignment]: a second "o", spelled out like the rest
OUTA 0x00
INIA 0x74 ; "t"
OUTA 0x00
INIA 0x65 ; "e"
OUTA 0x00
INIA 0x64 ; "d"
OUTA 0x00
INIA 0x0A
OUTA 0x00
HALT
+263
View File
@@ -0,0 +1,263 @@
; stage1.asm
; The first thing the machine runs. Reads the boot area off the disk and jumps into it.
;
; THIS IS THE PART THAT ONE DAY CANNOT BE CHANGED. It is written to go in ROM, so
; everything it knows has to be a thing that is true forever: which port the disk is on,
; that a SplitBit disk begins with its own name, and where two numbers sit in the block
; that name is in. It does not know what a file is, what a directory is, or that SBFS has
; versions. All of that lives in the boot area, on the disk, where it can be replaced.
;
; The test to apply to any line added here is the only test that matters for ROM: am I
; certain this is right forever? A loader that could find /System/cosmos.bin by name would
; be friendlier and would freeze the filesystem format in silicon.
;
; It is an ordinary boot image for now, so it can be run and tested with everything that
; already exists. Nothing about it changes when it moves into ROM except who puts it in
; memory.
;
; Written by Anachronaut
#Program
; ---- What is known forever ----
;
; Disk: 0x20 block high, 0x21 block low, 0x22 command, 0x23 status
; Controller: 0xE0 source bank, 0xE1/0xE2 source, 0xE3 dest bank, 0xE4/0xE5 dest,
; 0xE6/0xE7 count, 0xE8 command
; Banks: 0 Program Memory, 1 Data Memory, 3 the disk's buffer once registered
start:
; The disk's buffer becomes bank 3. Memory a device brings is reachable only through the
; controller, so this is what makes the block readable at all.
INIA 0d3
OUTA 0xE3
INIA 0x20
OUTA 0xE2
INIA 0x03
OUTA 0xE8
; Block 0, the superblock.
RSTA
SETD.0 BlockHigh
STA.0
SETD.0 BlockLow
STA.0
RCAL readBlock
BNQ bootFailed
; "SBFS", or there is nothing here to boot from. Four bytes, compared where they landed.
SETD.2 ScratchAt
LDD.0.2
SETD.2 DiskMagic
INIA 0d4
SETD.1 Counter
STA.1
magicLoop:
LDA.0
LDB.2
XOR
BNQ bootFailed
INCD.0
INCD.2
SETD.1 Counter
LDA.1
DECA
STA.1
BNA magicLoop
; Two numbers, at fixed offsets in the block whose name has just been checked: how many
; blocks a boot slot holds, and which of the two slots to start from.
SETD.2 ScratchAt
LDD.0.2
DPUP.0 0d14
LDA.0
BNA bootFailed ; A high byte means a slot larger than this will ever read.
INCD.0
LDA.0
BRA bootFailed ; No boot area at all, so this disk cannot be started.
SETD.1 SlotBlocks
STA.1
SETD.2 ScratchAt
LDD.0.2
DPUP.0 0d16
LDA.0
; The first block of the live slot. Slot 0 begins at block 1 and slot 1 begins a whole
; slot later, so the only arithmetic is an addition and there is nothing to multiply.
BRA slotZero
SETD.0 SlotBlocks
LDA.0
INCA
BRI slotFound
slotZero:
INIA 0d1
slotFound:
SETD.0 BlockLow
STA.0
RSTA
SETD.0 BlockHigh
STA.0
; And where it goes. Stage two lives above everything the system will occupy, so that
; loading the system does not walk over the loader while it is still running.
SETD.0 StageHigh
INIA 0xC0
STA.0
readLoop:
RCAL readBlock
BNQ bootFailed
; The block, out of the disk's buffer and into Program Memory where it will be run.
INIA 0d3
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2
RSTA ; splitlint[redundant-assignment]: a bank number, not the address above
OUTA 0xE3 ; DestBank: Program Memory.
SETD.0 StageHigh
LDA.0
OUTA 0xE4
RSTA
OUTA 0xE5
INIA 0x01
OUTA 0xE6
RSTA
OUTA 0xE7 ; A whole block.
INIA 0x01
OUTA 0xE8
; On to the next block, and the next page of Program Memory to put it in.
SETD.0 StageHigh
LDA.0
INCA
STA.0
SETD.0 BlockLow
LDA.0
INCA
STA.0
BNA blockStepped
SETD.0 BlockHigh
LDA.0
INCA
STA.0
blockStepped:
SETD.0 SlotBlocks
LDA.0
DECA
STA.0
BNA readLoop
; Into it. Nothing is checked about what was read, because there is nothing here that
; could check it: what a valid stage two looks like is stage two's business, and a ROM
; that knew would be a ROM that could be wrong about it later.
SETD.0 StageStart
LDD.1.0
BRD.1
; ---- Reading the block the two block registers name ----
;
; RCAL rather than CALL because what it hands back is Q, and an ordinary call would put
; back the registers this leaves its answer in.
readBlock:
SETD.0 BlockHigh
LDA.0
OUTA 0x20
SETD.0 BlockLow
LDA.0
OUTA 0x21
INIA 0x01
OUTA 0x22 ; Read.
waitDisk:
INA 0x23
INIB 0x01
AND
BRQ readDone ; The busy bit is down, so there is nothing to wait for.
WAIT
BRI waitDisk
readDone:
; The error bit, which is the whole of what can go wrong down here.
INIB 0x02
AND
BNQ readBad
; And into Data Memory, where the CPU can look at it.
INIA 0d3
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2
INIA 0d1
OUTA 0xE3 ; DestBank: Data Memory.
INIA 0x80
OUTA 0xE4
RSTA
OUTA 0xE5
INIA 0x01
OUTA 0xE6
RSTA
OUTA 0xE7
INIA 0x01
OUTA 0xE8
RSTA
RSTB
CCF
ADD ; Q is zero: the block is in Scratch.
RRET
readBad:
RSTA
INIB 0d1
CCF
ADD
RRET
; ---- When there is nothing to start ----
;
; One character and a stop. A ROM has no room for an explanation and nowhere to put one:
; the console is the only thing it can be sure of, and even that only in the sense that
; writing to a port nobody is listening to costs nothing.
bootFailed:
INIA 0x3F ; "?"
OUTA 0x00
INIA 0x0A
OUTA 0x00
HALT
#Data
DiskMagic:
"SBFS"
BlockHigh:
0x00
BlockLow:
0x00
SlotBlocks:
0x00
Counter:
0x00
; Where stage two is being written, a page at a time, and where it begins. The high byte is
; stepped as the blocks go by; the low byte is always zero, because a block is a page.
StageHigh:
0xC0
StageStart:
0xC0 0x00
; Where the superblock, and then each block of the boot area, lands on its way past.
;
; AN ADDRESS RATHER THAN STORAGE. Reserving it, or aligning to it, would put thirty two
; kilobytes of zeroes into a file that is going to be a ROM - which is what the assembler's
; own scratch map exists to avoid, for the same reason. Two bytes here say where; nothing
; carries what.
ScratchAt:
0x80 0x00
#Vectors
Boot start
+698
View File
@@ -0,0 +1,698 @@
; stage2.asm
; The second stage. Finds the system on the disk and starts it.
;
; Stage one knows nothing about filesystems and never will, because it is going into a ROM
; and a ROM that knew SBFS would freeze the format in silicon. This is where that knowledge
; lives instead: on the disk, in a boot slot, replaceable by writing blocks.
;
; ---- What it loads ----
;
; An ordinary SplitBit boot image, the same SPBT file the emulator has always been handed.
; That is the whole trick and it was the user's: a second stage that loads the machine's
; NORMAL image format is not a boot-specific mechanism at all. Bare metal SplitBit stops
; being a special case - a program that wants no operating system is just an image, written
; under CosmOS like any other, and startable because it is a file.
;
; ---- How it gets its own data ----
;
; Stage one places Program Memory and nothing else. A loadable image is written into the
; slot as code followed by data, so this program's Data Segment is sitting in Program Memory
; just past its own code when it starts, and the first thing it does is blit it down. See
; slotData.asm, which proves the mechanism on its own.
;
; Written by Anachronaut
#Program
; Above everything the system will occupy, so that loading the system does not walk over
; the loader while it is still running.
#Base 0xC000
start:
; ---- The Data Segment, fetched from just past the code ----
;
; A ROUND FOUR KILOBYTES rather than the exact size. The blit needs a length, the
; assembler will not work out the difference between two labels, and a hand kept number
; is a number that goes wrong quietly - slotData.asm padded to 257 and copied 256, and
; the byte that never arrived happened to be padding. Copying more than there is costs
; nothing: the source runs into slot padding, and the destination is nobody's memory.
RSTA
OUTA 0xE0 ; SourceBank: Program Memory, where stage one put everything.
SETD.0 codeEnd
PSHD.0
POPA
POPB
OUTB 0xE1
OUTA 0xE2
INIA 0d1
OUTA 0xE3 ; DestBank: Data Memory.
SETD.0 StageDataBase
PSHD.0
POPA
POPB
OUTB 0xE4
OUTA 0xE5
INIA 0x10
OUTA 0xE6
RSTA
OUTA 0xE7 ; Four kilobytes.
INIA 0x01
OUTA 0xE8
; ---- Now there is data, so there can be words ----
SETD.0 StageName
RCAL say
CALL sbfsMount
BNQ noFilesystem
; ---- What to start ----
;
; Read before the image is, because the configuration is staged where the image will go:
; there is one large free area down here and no reason to have two.
; ---- Something to start just this once ----
;
; A file naming an image to run instead of the configured system, and then to forget
; about. It is the same format as boot.cfg and is read the same way, because a second
; format for one setting would be a second format.
;
; CONSUMED BY BEING READ, not by working. It is deleted before the jump, which is the
; only moment there is: after the jump this program does not exist. That also means a
; one shot that hangs cannot hang twice - the request is already gone, and the mark on
; the disk brings the ordinary system back.
SETD.0 OncePath
SETD.2 StageAt
LDD.1.2
INIA 0d4
CALL cfgLoad
SETD.0 KeySystem
CALL cfgGet
BNQ noOnce
SETD.1 BootName
SETD.2 CfgValue
LDD.0.2
RCAL copyString
; Gone before it is used, so that whatever happens next happens only once.
SETD.0 OncePath
CALL sbfsDelete
SETD.0 OnceText
RCAL say
SETD.0 BootName
RCAL say
RCAL newLine
; ---- AND THE MARK IS NOT TOUCHED ----
;
; A one shot is already self limiting: the request was deleted a moment ago, so whatever
; happens now, the next start reads boot.cfg like any other. Marking it as well would
; report every successful bare metal boot as a start that never arrived - which is what
; the first version did, because a program that owns the whole machine has no filesystem
; to clear a mark with and is not doing anything wrong by not having one.
SETD.0 BootName
RCAL tryImage
; It did not start, and there is nothing to fall back to that was asked for. Whatever
; boot.cfg says is the thing to try, so carry on into it.
SETD.0 OnceFailedText
RCAL say
noOnce:
SETD.0 ConfigPath
SETD.2 StageAt
LDD.1.2
INIA 0d4
CALL cfgLoad
; Said before anything is started, so that a setting somebody meant which did not happen
; is on the screen above whatever happened instead.
SETD.0 KnownKeys
CALL cfgCheck
SETD.0 KeySystem
CALL cfgGet
SETD.1 BootName
BNQ systemDefault
SETD.2 CfgValue
LDD.0.2
RCAL copyString
BRI systemChosen
systemDefault:
SETD.0 SystemName
RCAL copyString
systemChosen:
; And what to fall back to, kept now because the configuration is about to be written
; over by the image itself.
RSTA
SETD.0 HaveFallback
STA.0
SETD.0 KeyFallback
CALL cfgGet
BNQ noFallbackSet
SETD.1 FallbackName
SETD.2 CfgValue
LDD.0.2
RCAL copyString
INIA 0x01
SETD.0 HaveFallback
STA.0
noFallbackSet:
; ---- How the last start went ----
;
; Settled means the last one arrived, so try what the configuration asks for. Trying
; means the loader handed over last time and nothing came back to say it got there, so
; the thing named is what broke the machine and the fallback is the way out. Fell back
; means that already happened and nobody has settled it since, so do the same again
; rather than retrying a system that is known not to start.
CALL sbfsBootState
SETD.0 SbfsStateWas
LDA.0
BRA stateSettled
INIB 0d1
XOR
BRQ stateFailedLast
SETD.0 StillBackText
RCAL say
BRI useFallback
stateFailedLast:
SETD.0 DidNotArriveText
RCAL say
INIA 0d2
CALL sbfsSetBootState
BRI useFallback
stateSettled:
; The mark goes down BEFORE the jump, because after it there is nothing here to do it.
; What clears it is the system reaching its prompt, which is not a claim that the system
; works - it is the moment a person can tell it otherwise.
INIA 0d1
CALL sbfsSetBootState
SETD.0 BootName
RCAL tryImage
; It did not start. Whatever went wrong has already said so, and if there is something
; else to try then trying it is the whole reason for having said it rather than stopping.
SETD.0 FallbackText
RCAL say
useFallback:
SETD.0 HaveFallback
LDA.0
BRA noFallbackTryAgain
SETD.0 FallbackName
RCAL tryImage
BRI noSystem
noFallbackTryAgain:
; NOTHING TO FALL BACK TO, so try what was asked for anyway rather than stopping. With no
; second name the mark is the only thing standing between the machine and its own
; configuration, and refusing on the strength of it would turn "the last start failed"
; into "no start is permitted", which is worse than the problem it was added to solve.
;
; A failure that was passing recovers here. 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.
SETD.0 NoFallbackText
RCAL say
SETD.0 BootName
RCAL tryImage
BRI noSystem
; ---- Starting one particular image ----
;
; DP0 names it. Returns only if it could not be started, having said why; everything that
; works ends in the jump at the bottom and never comes back.
tryImage:
SETD.1 TryName
STD.0.1
CALL sbfsFind
BNQ tryMissing
; ---- The image, read whole into somewhere nothing else is using ----
SETD.2 StageAt
LDD.1.2
CALL sbfsRead
BNQ unreadable
; "SPBT", or this is a file rather than something to start.
SETD.2 StageAt
LDD.0.2
SETD.2 BootMagic
INIA 0d4
RCAL matchTag
BNQ notAnImage
; The format version, and then four bytes of feature flags. A flag set is an image
; asking for something this machine may not have, and the honest answer to a request
; that cannot be understood is to refuse rather than to run it anyway.
LDA.0
INIB 0d1
XOR
BNQ wrongVersion
INCD.0
INIA 0d4
RCAL allZero
BNQ wantsMore
; "PRG", its length, and then the code itself straight into Program Memory.
SETD.2 ProgTag
INIA 0d3
RCAL matchTag
BNQ notAnImage
RCAL takeCount
RSTA
RCAL placeSegment
; "DAT", the same again into Data Memory.
SETD.2 DataTag
INIA 0d3
RCAL matchTag
BNQ notAnImage
RCAL takeCount
INIA 0d1
RCAL placeSegment
; ---- "VEC", which an image written before vectors existed simply does not have ----
;
; So running out of file here is the ordinary case rather than a fault: what follows is
; the end of a perfectly good image, and the entry point is then the address zero.
SETD.2 VecTag
INIA 0d3
RCAL matchTag
BNQ noVectors
RCAL takeCount
vectorLoop:
SETD.2 CountHigh
LDA.2
INCD.2
LDB.2
OR
BRQ vectorsDone
; Four bytes: where in Program Memory the vector sits, then what to put there. The
; controller writes it, because nothing else can write Program Memory.
LDA.0
SETD.2 VecWhere
STA.2
INCD.0
LDA.0
INCD.2
STA.2
INCD.0
RSTA
OUTA 0xE3 ; DestBank: Program Memory.
SETD.2 VecWhere
LDA.2
OUTA 0xE4
INCD.2
LDA.2
OUTA 0xE5
LDA.0
OUTA 0xE9 ; A byte straight in, rather than a blit of one.
INCD.0
LDA.0
OUTA 0xE9
INCD.0
; THE BOOT VECTOR IS THE ONE WORTH KEEPING. Program Memory cannot be read back, so the
; address to start at has to be noticed on its way past rather than looked up after.
SETD.2 VecWhere
LDA.2
INIB 0xFC
XOR
BNQ vectorStepped
INCD.2
LDA.2
BNA vectorStepped
SETD.2 EntryHigh
DECD.0
DECD.0
LDA.0
STA.2
INCD.0
LDA.0
INCD.2
STA.2
INCD.0
vectorStepped:
SETD.2 CountLow
LDA.2
INIB 0d4
CCF
SUB
MVQA
STA.2
BNC vectorLoop
SETD.2 CountHigh
LDA.2
DECA
STA.2
BRI vectorLoop
noVectors:
vectorsDone:
; And into it. Everything above has been arranging memory; this is the only instruction
; that hands the machine over.
SETD.0 EntryHigh
LDD.1.0
BRD.1
unreadable:
SETD.0 UnreadableText
RCAL say
RCAL sayName
RRET
notAnImage:
SETD.0 NotImageText
RCAL say
RCAL sayName
RRET
wrongVersion:
SETD.0 VersionText
RCAL say
RCAL sayName
RRET
wantsMore:
SETD.0 FeatureText
RCAL say
RCAL sayName
RRET
noFilesystem:
SETD.0 NoDiskText
RCAL say
HALT
tryMissing:
SETD.0 NoSystemText
RCAL say
RCAL sayName
RRET
; Whichever image was being tried, said after the reason it did not work.
sayName:
SETD.0 TryName
LDD.0.0
RCAL say
RCAL newLine
RRET
noSystem:
SETD.0 NothingText
RCAL say
HALT
; DP0 names a string and DP1 where to put it, terminator and all.
copyString:
LDA.0
STA.1
BRA copyDone
INCD.0
INCD.1
BRI copyString
copyDone:
RRET
; ---- Comparing a marker and stepping past it ----
;
; DP0 is in the image, DP2 names what it should say, A is how many bytes. DP0 is left past
; them either way, which is what lets a caller try one marker and carry on.
matchTag:
SETD.1 TagLeft
STA.1
tagLoop:
SETD.1 TagLeft
LDA.1
BRA tagSame
DECA
STA.1
LDA.0
LDB.2
XOR
BNQ tagDiffers
INCD.0
INCD.2
BRI tagLoop
tagSame:
RSTA
RSTB
CCF
ADD
RRET
tagDiffers:
; Past the rest of it regardless, so that what DP0 points at does not depend on which
; byte happened to differ.
SETD.1 TagLeft
LDA.1
BRA tagDiffersDone
DECA
STA.1
INCD.0
BRI tagDiffers
tagDiffersDone:
RSTA
INIB 0d1
CCF
ADD
RRET
; A bytes at DP0, all of which have to be zero. Used for the feature flags, where anything
; set is the image asking for something this machine may not be able to give it.
allZero:
SETD.1 TagLeft
STA.1
zeroLoop:
SETD.1 TagLeft
LDA.1
BRA zeroAllClear
DECA
STA.1
LDA.0
INCD.0
BNA zeroNotClear
BRI zeroLoop
zeroAllClear:
RSTA
RSTB
CCF
ADD
RRET
zeroNotClear:
RSTA
INIB 0d1
CCF
ADD
RRET
; Two bytes at DP0, most significant first, into the count that placeSegment and the vector
; loop both work through. DP0 is left on the byte after them.
takeCount:
LDA.0
SETD.1 CountHigh
STA.1
INCD.0
LDA.0
SETD.1 CountLow
STA.1
INCD.0
RRET
; ---- A segment, out of the staged image and into the memory it belongs in ----
;
; A is the bank: 0 for Program Memory, 1 for Data. Both segments go to address zero, which
; is what a boot image means - it is the format for something that owns the machine.
;
; DP0 is left past the segment, ready for whatever marker comes next.
placeSegment:
SETD.1 WantBank
STA.1
INIA 0d1
OUTA 0xE0 ; SourceBank: Data Memory, where the image was staged.
PSHD.0
POPA
POPB
OUTB 0xE1
OUTA 0xE2
SETD.1 WantBank
LDA.1
OUTA 0xE3
RSTA
OUTA 0xE4
OUTA 0xE5 ; To the bottom of it.
SETD.1 CountHigh
LDA.1
OUTA 0xE6
INCD.1
LDA.1
OUTA 0xE7
INIA 0x01
OUTA 0xE8
; And past it. DPUW steps a pointer by A and B together, which is exactly the shape the
; two halves of a length are already in.
SETD.1 CountHigh
LDA.1
INCD.1
LDB.1
DPUW.0
RRET
; ---- Saying something ----
;
; Its own rather than console.asm's, because everything this includes has to fit in a boot
; slot and the console library brings a great deal this will never use. DP0 names a string.
say:
LDA.0
BRA sayDone
OUTA 0x00
INCD.0
BRI say
sayDone:
RRET
newLine:
INIA 0x0A
OUTA 0x00
RRET
#Data
; Out of the way of everything: the system's data goes to 0x0000, and whatever image is
; being loaded is staged below this.
#Base 0xE000
; The first thing in the segment, so that its address is where the blit above puts the
; whole of it.
StageDataBase:
StageName:
"stage two
"
FoundText:
"found "
NoDiskText:
"no filesystem
"
NoSystemText:
"no "
; What to start. A name for now; the plan is for this to be read out of a file so that any
; number of systems can sit on one disk and the choice is an ordinary safe write.
; Where the configuration lives, and what to start when it does not say.
ConfigPath:
"/System/Boot/boot.cfg"
OncePath:
"/System/Boot/once.cfg"
SystemName:
"/System/Boot/cosmos.bin"
KeySystem:
"system"
KeyFallback:
"fallback"
; The keys this knows. Anything else in the file is a setting nothing asked for, and
; cfgCheck says so rather than letting it look as though it worked.
KnownKeys:
"system"
"fallback"
""
FallbackText:
"trying the fallback
"
DidNotArriveText:
"the last start did not arrive
"
StillBackText:
"still on the fallback: settle it to try again
"
NoFallbackText:
"no fallback, so trying it again
"
NothingText:
"nothing to start
"
OnceText:
"just this once: "
OnceFailedText:
"it did not start, so carrying on
"
BootName:
#Reserve 0d128
FallbackName:
#Reserve 0d128
HaveFallback:
0x00
TryName:
0x00 0x00
UnreadableText:
"could not read it
"
NotImageText:
"not a boot image
"
VersionText:
"an image this cannot start
"
FeatureText:
"the image wants more machine
"
BootMagic:
"SPBT"
ProgTag:
"PRG"
DataTag:
"DAT"
VecTag:
"VEC"
TagLeft:
0x00
WantBank:
0x00
CountHigh:
0x00
CountLow:
0x00
VecWhere:
0x00 0x00
; Where to start once memory is arranged. Zero unless the image says otherwise, which is
; what a boot image with no vector table means and what every program written before the
; table existed relies on.
EntryHigh:
0x00
EntryLow:
0x00
; Where the image is put while it is being taken apart. Below this program's own data and
; above everything the system will occupy. An address rather than storage: reserving it
; would put its zeroes in the boot slot.
StageAt:
0x20 0x00
#Include sbfs.asm
#Include text.asm
#Include config.asm
; ---- The end of everything ----
;
; Back to the Program Segment so that this label lands past the included code as well as
; past the code above. It is where the Data Segment's image begins once stage one has put
; the whole slot into Program Memory.
#Program
codeEnd:
+35
View File
@@ -0,0 +1,35 @@
; wedged.asm
; A system that starts and never reaches a prompt.
;
; Not a broken program so much as a stand in for one: what matters is that it is handed the
; machine and never clears the mark the loader put on the disk, which is what every real
; way of failing before the shell has in common.
;
; Without that mark, pointing /System/Boot/boot.cfg at something like this would be a
; machine that could not be told anything ever again - the shell is the only way to change
; the file, and the file is what stops the shell from starting.
;
; Written by Anachronaut
#Program
start:
SETD.0 Dying
sayLoop:
LDA.0
BRA gone
OUTA 0x00
INCD.0
BRI sayLoop
gone:
HALT
#Data
Dying:
"a system that never reaches a prompt
"
#Vectors
Boot start
+24 -2
View File
@@ -17,12 +17,29 @@
; Correct output is two stops. A and B differ between them, the addresses differ, and the ; Correct output is two stops. A and B differ between them, the addresses differ, and the
; Stack Pointer differs too, because the second one is inside a subroutine and a call has ; Stack Pointer differs too, because the second one is inside a subroutine and a call has
; put ten bytes down by then. ; put ten bytes down by then.
;
; EVERY POINTER IS SET BEFORE EACH STOP, and the three this program owns are rotated
; between the two so that all of them visibly change.
;
; This program used to leave DP1 and DP2 alone, and what a stop then showed for them was
; whatever the shell happened to have left there. That is a real thing about the machine -
; a program is handed the pointers as it finds them - but it is not this program's to
; demonstrate, and it made what this program prints depend on where CosmOS's code happens
; to sit. The recorded output had to be taken again four times in one day's work, every
; time for a value nothing should ever depend on.
;
; A demonstration of what the registers were should show registers somebody chose. Then
; every line of it is being asserted rather than merely observed.
;
; DP3 is deliberately still the system's. It is where the program was entered, which is the
; one thing here worth seeing that this program did not choose, and it is steady because it
; is this program's own base.
#Include services.asm #Include services.asm
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
SETD.0 Banner SETD.0 Banner
@@ -31,6 +48,8 @@ start:
INIA 0d17 INIA 0d17
INIB 0d34 INIB 0d34
SETD.0 Marker SETD.0 Marker
SETD.1 Banner
SETD.2 DoneText
SWI osBreak SWI osBreak
; The second stop is inside a subroutine, so that the Stack Pointer is visibly not where ; The second stop is inside a subroutine, so that the Stack Pointer is visibly not where
@@ -39,18 +58,21 @@ start:
SETD.0 DoneText SETD.0 DoneText
SWI osPrintString SWI osPrintString
RSTA
SWI osExit SWI osExit
deeper: deeper:
INIA 0d68 INIA 0d68
INIB 0d85 INIB 0d85
SETD.0 Banner SETD.0 Banner
SETD.1 DoneText
SETD.2 Marker
SWI osBreak SWI osBreak
RET RET
#Data #Data
#Base 0x1000 #Base 0x2000
Banner: Banner:
"two stops, and what the registers were at each "two stops, and what the registers were at each
+107
View File
@@ -0,0 +1,107 @@
; Tries to commit a file bigger than the room it reserved.
;
; osFileStart sets aside an extent and osFileWrite refuses a block index outside it, so the
; obvious way to reach a neighbouring file - writing off the end - is already barred. This
; is the other way to the same place: reserve one block, write the one block, and then tell
; osFileDone the file came to two.
;
; NOTHING WOULD SAY SO IF THAT WERE ALLOWED. A directory entry is the only record of what a
; file owns, so an entry claiming a block it was never given simply owns it, and whatever
; owned it before owns it too. Both files then look perfectly well formed.
;
; Correct behaviour is a refusal, and the file left as it was. The reservation is one block
; and a tail of ten, so:
;
; two blocks and no tail more than was reserved refused
; one block and a tail exactly what was reserved allowed
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
; One block, and ten bytes after it.
SETD.0 Name
SETD.3 0x00 0x01
INIA 0d10
SWI osFileStart
BNQ noStart
SETD.1 Block
RSTA
RSTB
SWI osFileWrite
BNQ noWrite
SETD.1 Block
RSTA
INIB 0d1
SWI osFileWrite
BNQ noWrite
; Two whole blocks, which is more than one block and a tail.
SETD.3 0x00 0x02
RSTA
SWI osFileDone
BNQ refused
SETD.0 Allowed
SWI osPrintString
RSTA
SWI osExit
refused:
SETD.0 Refused
SWI osPrintString
; And the honest size, which is what was reserved.
SETD.3 0x00 0x01
INIA 0d10
SWI osFileDone
BNQ noHonest
SETD.0 Honest
SWI osPrintString
RSTA
SWI osExit
noHonest:
SETD.0 NoHonest
SWI osPrintString
INIA 0d1
SWI osExit
noStart:
SETD.0 NoStart
SWI osPrintString
INIA 0d1
SWI osExit
noWrite:
SETD.0 NoWrite
SWI osPrintString
INIA 0d1
SWI osExit
#Data
#Base 0x2000
Name:
"claim.dat"
Block:
#Reserve 0d256
Allowed:
"claiming more than was reserved was ALLOWED
"
Refused:
"claiming more than was reserved was refused
"
Honest:
"and the size it really came to was taken
"
NoHonest:
"the honest size was refused too
"
NoStart:
"no start
"
NoWrite:
"no write
"
+284
View File
@@ -0,0 +1,284 @@
; Compare two files without asking either one to fit in Data Memory.
;
; Each occupied block is read into its own 256-byte buffer. The byte count returned for
; the block is compared before its contents, and only those bytes are examined: bytes
; after the end of a short final block belong to neither file and are allowed to differ.
;
; Written by ChatGPT for Anachronaut's SplitBit
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Arguments
INIB 0xFF
SWI osArgument
SETD.0 Arguments
LDA.0
BRA usage
CALL textSplit
SETD.0 TextRest
LDD.0.0
LDA.0
BRA usage
SETD.0 Arguments
SWI osFileInfo
BNQ firstFailed
SETD.0 FirstBlocks
STD.3.0
SETD.0 TextRest
LDD.0.0
SWI osFileInfo
BNQ secondFailed
SETD.0 SecondBlocks
STD.3.0
; A different number of occupied blocks is immediately a different file.
SETD.0 FirstBlocks
LDA.0
SETD.1 SecondBlocks
LDB.1
CCF
SUB
BNQ different
INCD.0
INCD.1
LDA.0
LDB.1
CCF
SUB
BNQ different
SETD.0 FirstBlocks
SETD.1 RemainingBlocks
CALL copyWord
RSTA
SETD.0 Index
STA.0
INCD.0
STA.0
nextBlock:
SETD.0 RemainingBlocks
LDA.0
INCD.0
LDB.0
OR
BRQ alike
SETD.0 Arguments
SETD.1 FirstBlock
CALL readAtIndex
BNQ firstReadFailed
SETD.0 FirstCount
STD.3.0
SETD.0 TextRest
LDD.0.0
SETD.1 SecondBlock
CALL readAtIndex
BNQ secondReadFailed
SETD.0 SecondCount
STD.3.0
; Equal occupied-block counts do not imply equal tails, so compare the valid byte count
; returned for this block as well.
SETD.0 FirstCount
LDA.0
SETD.1 SecondCount
LDB.1
CCF
SUB
BNQ different
INCD.0
INCD.1
LDA.0
LDB.1
CCF
SUB
BNQ different
SETD.0 FirstCount
SETD.1 BytesLeft
CALL copyWord
SETD.0 FirstBlock
SETD.1 SecondBlock
compareByte:
SETD.2 BytesLeft
LDA.2
INCD.2
LDB.2
OR
BRQ blockSame
LDA.0
LDB.1
CCF
SUB
BNQ different
INCD.0
INCD.1
SETD.2 BytesLeft
CALL takeByte
BRI compareByte
blockSame:
CALL stepIndex
CALL takeBlock
BRI nextBlock
; DP0 is the path and DP1 the destination buffer. Q and DP3 are the service answers.
readAtIndex:
SETD.2 Index
LDA.2
INCD.2
LDB.2
SWI osFileBlock
RET
; DP0 names the source word and DP1 the destination word.
copyWord:
LDA.0
STA.1
INCD.0
INCD.1
LDA.0
STA.1
RET
stepIndex:
SETD.0 Index
INCD.0
LDA.0
INCA
STA.0
BNC stepDone
DECD.0
LDA.0
INCA
STA.0
stepDone:
RET
takeBlock:
SETD.0 RemainingBlocks
INCD.0
LDA.0
BRA blockBorrow
DECA
STA.0
RET
blockBorrow:
INIA 0xFF
STA.0
DECD.0
LDA.0
DECA
STA.0
RET
; Decrement the big-endian sixteen-bit BytesLeft. A full block arrives as 0x0100, so an
; eight-bit counter would compare none of it and call many different files equal.
takeByte:
INCD.2
LDA.2
BRA byteBorrow
DECA
STA.2
RET
byteBorrow:
INIA 0xFF
STA.2
DECD.2
LDA.2
DECA
STA.2
RET
alike:
SETD.0 SameText
SWI osPrintString
RSTA
SWI osExit
different:
SETD.0 DifferentText
SWI osPrintString
INIA 0d1
SWI osExit
usage:
SETD.0 Usage
SWI osPrintString
INIA 0d2
SWI osExit
firstFailed:
SETD.0 FirstError
SWI osPrintString
INIA 0d2
SWI osExit
secondFailed:
SETD.0 SecondError
SWI osPrintString
INIA 0d2
SWI osExit
firstReadFailed:
SETD.0 FirstReadError
SWI osPrintString
INIA 0d2
SWI osExit
secondReadFailed:
SETD.0 SecondReadError
SWI osPrintString
INIA 0d2
SWI osExit
#Data
#Base 0x2000
Arguments:
#Reserve 0d256
FirstBlocks:
0x00 0x00
SecondBlocks:
0x00 0x00
RemainingBlocks:
0x00 0x00
Index:
0x00 0x00
FirstCount:
0x00 0x00
SecondCount:
0x00 0x00
BytesLeft:
0x00 0x00
FirstBlock:
#Reserve 0d256
SecondBlock:
#Reserve 0d256
Usage:
"compare: give me two files
"
FirstError:
"compare: cannot find the first file
"
SecondError:
"compare: cannot find the second file
"
FirstReadError:
"compare: cannot read the first file
"
SecondReadError:
"compare: cannot read the second file
"
SameText:
"the same
"
DifferentText:
"different
"
#Include text.asm
+276
View File
@@ -0,0 +1,276 @@
; Copy one file to another without asking either file to fit in Data Memory.
;
; The two paths are the two words in the argument. Filesystem names can contain spaces,
; but the CosmOS command line has no quoting yet, so this deliberately has the same rule
; as the shell: a space separates things.
;
; osFileInfo reports how many disk blocks the source occupies, while osFileStart wants the
; shape stored in an SBFS entry: whole blocks and a possible tail. Reading the last block
; first recovers that distinction. The ordinary copy then walks forward one block at a
; time, through one 256-byte buffer.
;
; Written by ChatGPT for Anachronaut's SplitBit
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Arguments
INIB 0xFF
SWI osArgument
SETD.0 Arguments
LDA.0
BRA usage
CALL textSplit
SETD.0 TextRest
LDD.0.0
LDA.0
BRA usage
; How many occupied blocks the source has. Keep it as both the loop bound and the
; distinction between an empty file and one whose last block must be inspected.
SETD.0 Arguments
SWI osFileInfo
BNQ sourceFailed
SETD.0 SourceBlocks
STD.3.0
PSHD.3
POPB
POPA
OR
BRQ emptySource
; Read the final occupied block. DP3 says how many bytes of it belong to the file:
; 0x0100 for a whole block, or 0x0001 through 0x00FF for a tail.
SETD.0 SourceBlocks
LDD.3.0
DECD.3
SETD.0 Index
STD.3.0
SETD.0 Arguments
SETD.1 Block
SETD.2 Index
LDA.2
INCD.2
LDB.2
SWI osFileBlock
BNQ readFailed
SETD.0 LastCount
STD.3.0
; A full final block means every occupied block is whole. A short final block means
; there is one fewer whole block and its low-byte count is the tail.
SETD.0 LastCount
LDA.0
BNA wholeEnding
SETD.0 Index
SETD.1 OutputWhole
CALL copyWord
SETD.0 LastCount
INCD.0
LDA.0
SETD.1 OutputTail
STA.1
BRI sizeKnown
wholeEnding:
SETD.0 SourceBlocks
SETD.1 OutputWhole
CALL copyWord
RSTA
SETD.0 OutputTail
STA.0
sizeKnown:
BRI startOutput
emptySource:
RSTA
SETD.0 OutputWhole
STA.0
INCD.0
STA.0
SETD.0 OutputTail
STA.0
startOutput:
SETD.0 TextRest
LDD.0.0
SETD.2 OutputWhole
LDD.3.2
SETD.2 OutputTail
LDA.2
SWI osFileStart
BNQ startFailed
; Empty files have no blocks to transfer, but still need committing so that an existing
; destination becomes an empty file.
SETD.0 SourceBlocks
SETD.1 RemainingBlocks
CALL copyWord
RSTA
SETD.0 Index
STA.0
INCD.0
STA.0
copyNext:
SETD.0 RemainingBlocks
LDA.0
INCD.0
LDB.0
OR
BRQ copyDone
SETD.0 Arguments
SETD.1 Block
SETD.2 Index
LDA.2
INCD.2
LDB.2
SWI osFileBlock
BNQ readFailed
SETD.1 Block
SETD.2 Index
LDA.2
INCD.2
LDB.2
SWI osFileWrite
BNQ writeFailed
CALL stepIndex
CALL takeBlock
BRI copyNext
copyDone:
SETD.2 OutputWhole
LDD.3.2
SETD.2 OutputTail
LDA.2
SWI osFileDone
BNQ doneFailed
SETD.0 Copied
SWI osPrintString
RSTA
SWI osExit
; DP0 names the source word and DP1 the destination word.
copyWord:
LDA.0
STA.1
INCD.0
INCD.1
LDA.0
STA.1
RET
; Increment the big-endian sixteen-bit Index.
stepIndex:
SETD.0 Index
INCD.0
LDA.0
INCA
STA.0
BNC stepDone
DECD.0
LDA.0
INCA
STA.0
stepDone:
RET
; Decrement the big-endian sixteen-bit RemainingBlocks.
takeBlock:
SETD.0 RemainingBlocks
INCD.0
LDA.0
BRA takeBorrow
DECA
STA.0
RET
takeBorrow:
INIA 0xFF
STA.0
DECD.0
LDA.0
DECA
STA.0
RET
usage:
SETD.0 Usage
SWI osPrintString
INIA 0d2
SWI osExit
sourceFailed:
SETD.0 SourceError
SWI osPrintString
INIA 0d1
SWI osExit
readFailed:
SETD.0 ReadError
SWI osPrintString
INIA 0d1
SWI osExit
startFailed:
SETD.0 StartError
SWI osPrintString
INIA 0d1
SWI osExit
writeFailed:
SETD.0 WriteError
SWI osPrintString
INIA 0d1
SWI osExit
doneFailed:
SETD.0 DoneError
SWI osPrintString
INIA 0d1
SWI osExit
#Data
#Base 0x2000
Arguments:
#Reserve 0d256
SourceBlocks:
0x00 0x00
RemainingBlocks:
0x00 0x00
Index:
0x00 0x00
LastCount:
0x00 0x00
OutputWhole:
0x00 0x00
OutputTail:
0x00
Block:
#Reserve 0d256
Usage:
"copy: give me a source and destination
"
SourceError:
"copy: cannot find the source
"
ReadError:
"copy: cannot read the source
"
StartError:
"copy: cannot create the destination
"
WriteError:
"copy: cannot write the destination
"
DoneError:
"copy: cannot finish the destination
"
Copied:
"copied
"
#Include text.asm
+64 -11
View File
@@ -51,7 +51,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
SETD.0 FileName SETD.0 FileName
@@ -75,8 +75,15 @@ start:
RSTA RSTA
STA.0 STA.0
RSTA
SETD.0 TooLong
STA.0
CALL loadFile CALL loadFile
SETD.0 TooLong
LDA.0
BNA tooLongToEdit
SETD.0 FileName SETD.0 FileName
SWI osPrintString SWI osPrintString
SETD.0 CommaText SETD.0 CommaText
@@ -161,12 +168,21 @@ commandArgument:
BRI commandLoop BRI commandLoop
quit: quit:
RSTA
SWI osExit
tooLongToEdit:
SETD.0 TooLongText
SWI osPrintString
CALL newLine
INIA 0d1
SWI osExit SWI osExit
noName: noName:
SETD.0 NoNameText SETD.0 NoNameText
SWI osPrintString SWI osPrintString
CALL newLine CALL newLine
INIA 0d2
SWI osExit SWI osExit
@@ -192,7 +208,7 @@ insertLoop:
SETD.0 EnteringText SETD.0 EnteringText
SWI osPrintString SWI osPrintString
SETD.0 Entry SETD.0 Entry
INIB 0d80 INIB 0d128
SWI osReadLine SWI osReadLine
INA 0x01 INA 0x01
INIB 0x02 ; ENDED INIB 0x02 ; ENDED
@@ -232,7 +248,7 @@ doChange:
SETD.0 EnteringText SETD.0 EnteringText
SWI osPrintString SWI osPrintString
SETD.0 Entry SETD.0 Entry
INIB 0d80 INIB 0d128
SWI osReadLine SWI osReadLine
INA 0x01 INA 0x01
INIB 0x02 ; ENDED INIB 0x02 ; ENDED
@@ -529,7 +545,6 @@ listStep:
PSHD.3 PSHD.3
POPD.0 POPD.0
DPUP.0 0d03 DPUP.0 0d03
SETD.1 Leftover
LDA.1 LDA.1
BRA listEmpty BRA listEmpty
listChars: listChars:
@@ -599,6 +614,29 @@ splitStep:
XOR XOR
BRQ splitLine BRQ splitLine
; ---- Room for it ----
;
; THERE WAS NO CHECK HERE AT ALL, and Entry is followed in memory by TextHead and
; ArenaFree - the head of the document and the pointer the line allocator hands out. A
; line longer than the buffer wrote characters over both, so the list head pointed into
; the middle of the text and the allocator handed out an address inside the file.
;
; What that looked like: a thirty one line file opened as three, one of them cut short.
; Then, opening it a second time, a list that led back into itself and a machine that
; walked it for ever - the emulator still running and the machine never answering again.
;
; Typing a long line was always safe, because osReadLine is told how much room there is.
; Only the file being read went unchecked, which is why a new document behaved and a
; source file did not.
PSHA
SETD.2 EntryLength
LDA.2
INIB 0d128
CCF
SUB
POPA ; The character back, and Q still says whether there is room.
BRQ splitTooLong
STA.1 ; A is still the character; an ALU operation does not touch it. STA.1 ; A is still the character; an ALU operation does not touch it.
INCD.1 INCD.1
SETD.2 EntryLength SETD.2 EntryLength
@@ -626,6 +664,15 @@ splitOn:
CALL takeOneOff CALL takeOneOff
BRI splitStep BRI splitStep
splitTooLong:
; NOT TRUNCATED. This is an editor: a line it shortened here would be written back
; shortened, and the file would be damaged by having been looked at. Refusing leaves it
; exactly as it was.
INIA 0x01
SETD.2 TooLong
STA.2
RET
splitLast: splitLast:
; A file that does not end in a newline still has a last line in it. ; A file that does not end in a newline still has a last line in it.
SETD.2 EntryLength SETD.2 EntryLength
@@ -653,12 +700,12 @@ appendNode:
; DP2 is a two byte count. Takes one off it. ; DP2 is a two byte count. Takes one off it.
takeOneOff: takeOneOff:
DPUP.2 0d01 INCD.2
LDA.2 LDA.2
DECA DECA
STA.2 STA.2
BNC takeOneDone ; No borrow, so the high half is untouched. BNC takeOneDone ; No borrow, so the high half is untouched.
DPDN.2 0d01 DECD.2
LDA.2 LDA.2
DECA DECA
STA.2 STA.2
@@ -723,7 +770,6 @@ writeOut:
CCF CCF
SUB SUB
MVQA MVQA
SETD.0 WroteSize
STA.0 ; WroteSize is now a count of bytes, which is what gets printed. STA.0 ; WroteSize is now a count of bytes, which is what gets printed.
; And into the two registers the service takes a size in. ; And into the two registers the service takes a size in.
@@ -766,12 +812,12 @@ printWord:
; DP0 is a two byte number, A is a byte. Adds the one to the other. ; DP0 is a two byte number, A is a byte. Adds the one to the other.
addByteToWord: addByteToWord:
DPUP.0 0d01 INCD.0
LDB.0 LDB.0
CCF CCF
ADD ADD
STQ.0 STQ.0
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
RSTB RSTB
ADD ADD
@@ -780,7 +826,7 @@ addByteToWord:
#Data #Data
#Base 0x1000 #Base 0x2000
Break: Break:
0x0A 0x00 0x0A 0x00
@@ -812,13 +858,20 @@ NeedsLineText:
"which line?" "which line?"
NoWriteText: NoWriteText:
"it would not write" "it would not write"
TooLongText:
"a line in it is longer than this can edit, so it has not been opened"
FileName: FileName:
#Reserve 0d24 #Reserve 0d24
Command: Command:
#Reserve 0d41 #Reserve 0d41
; A hundred and twenty eight and the zero that ends it, which is what a line is everywhere
; else on this machine - the same number configuration files use, rather than a second
; answer to a question already answered.
Entry: Entry:
#Reserve 0d81 #Reserve 0d129
TooLong:
0x00
TextHead: TextHead:
0x00 0x00 0x00 0x00
+3 -2
View File
@@ -4,7 +4,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
; Swap ValueB and ValueA. ; Swap ValueB and ValueA.
@@ -67,11 +67,12 @@ start:
BRI start BRI start
end: end:
CALL lineFeed CALL lineFeed
RSTA
SWI osExit SWI osExit
#Data #Data
#Base 0x1000 #Base 0x2000
ValueA: ValueA:
; Low byte, high byte. ; Low byte, high byte.
+3 -2
View File
@@ -4,7 +4,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
; Swap ValueB and ValueA. ; Swap ValueB and ValueA.
@@ -123,11 +123,12 @@ start:
end: end:
CALL lineFeed CALL lineFeed
;HALT ;HALT
RSTA
SWI osExit SWI osExit
#Data #Data
#Base 0x1000 #Base 0x2000
ValueA: ValueA:
; Lowest byte ... Highest byte. ; Lowest byte ... Highest byte.
+5 -5
View File
@@ -4,11 +4,11 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
; Load our initial values into A and B. ; Load our initial values into A and B.
INIA 0x00 RSTA
CALL printByteDecimal CALL printByteDecimal
CALL blankSpace CALL blankSpace
; Move the value into B. ; Move the value into B.
@@ -25,8 +25,7 @@ start:
PSHA PSHA
POPB POPB
; Copy Q into A ; Copy Q into A
PSHQ MVQA
POPA
; Print A. ; Print A.
CALL printByteDecimal CALL printByteDecimal
CALL blankSpace CALL blankSpace
@@ -34,10 +33,11 @@ start:
end: end:
CALL lineFeed CALL lineFeed
;HALT ;HALT
RSTA
SWI osExit ; Return to CosmOS. SWI osExit ; Return to CosmOS.
#Data #Data
#Base 0x1000 #Base 0x2000
#Include print.asm #Include print.asm
+8 -2
View File
@@ -24,7 +24,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
; ---- Write it ---- ; ---- Write it ----
@@ -101,33 +101,39 @@ atEnd:
BRQ stillThere BRQ stillThere
SETD.0 GoneText SETD.0 GoneText
SWI osPrintString SWI osPrintString
RSTA
SWI osExit SWI osExit
stillThere: stillThere:
SETD.0 StillText SETD.0 StillText
SWI osPrintString SWI osPrintString
RSTA
SWI osExit SWI osExit
noSave: noSave:
SETD.0 NoSaveText SETD.0 NoSaveText
SWI osPrintString SWI osPrintString
INIA 0d1
SWI osExit SWI osExit
noRead: noRead:
SETD.0 NoReadText SETD.0 NoReadText
SWI osPrintString SWI osPrintString
INIA 0d1
SWI osExit SWI osExit
noRename: noRename:
SETD.0 NoRenameText SETD.0 NoRenameText
SWI osPrintString SWI osPrintString
INIA 0d1
SWI osExit SWI osExit
noDelete: noDelete:
SETD.0 NoDeleteText SETD.0 NoDeleteText
SWI osPrintString SWI osPrintString
INIA 0d1
SWI osExit SWI osExit
#Data #Data
#Base 0x1000 #Base 0x2000
Name: Name:
"kept.txt" "kept.txt"
+7 -4
View File
@@ -17,7 +17,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
CIF ; Nothing arrives until there is something to catch it. CIF ; Nothing arrives until there is something to catch it.
@@ -39,14 +39,16 @@ start:
OUTA 0x02 OUTA 0x02
SIF SIF
wait: ; Called spin rather than wait because WAIT is an instruction now, and a label may not
; be one. Which is the joke of it: this loop is exactly what WAIT exists to replace.
spin:
; This loop is the point. It never touches the console, so every character that appears ; This loop is the point. It never touches the console, so every character that appears
; below was put there by something that interrupted it. ; below was put there by something that interrupted it.
SETD.3 Stopping SETD.3 Stopping
LDA.3 LDA.3
RSTB RSTB
OR OR
BRQ wait BRQ spin
CALL newLine CALL newLine
RSTA RSTA
@@ -54,6 +56,7 @@ wait:
SETD.0 DoneText SETD.0 DoneText
CALL printString CALL printString
CALL newLine CALL newLine
RSTA
SWI osExit SWI osExit
; Entered because the console had something to say. Never called. ; Entered because the console had something to say. Never called.
@@ -82,7 +85,7 @@ keyStop:
#Data #Data
#Base 0x1000 #Base 0x2000
Banner: Banner:
"keys, by interrupt. q stops." "keys, by interrupt. q stops."
+3 -2
View File
@@ -32,7 +32,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
CALL seedGlider CALL seedGlider
@@ -105,6 +105,7 @@ lifeEnd:
CALL lineFeed CALL lineFeed
CALL printString ; DP0 still holds the words: CALL puts DP0 back. CALL printString ; DP0 still holds the words: CALL puts DP0 back.
CALL lineFeed CALL lineFeed
RSTA
SWI osExit SWI osExit
seedGlider: seedGlider:
@@ -348,7 +349,7 @@ delayDone:
#Data #Data
#Base 0x1000 #Base 0x2000
RowCount: RowCount:
0x00 0x00
+159
View File
@@ -0,0 +1,159 @@
; Read a text file one screen at a time.
;
; Twenty two lines are shown before a prompt. Space advances another screen, Return one
; line, and q gives the machine back to CosmOS. This is forward-only on purpose: the file
; stream holds one block and never asks the whole document to fit in memory.
;
; Written by ChatGPT for Anachronaut's SplitBit
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Name
INIB 0d29
SWI osArgument
SETD.0 Name
LDA.0
BRA noName
CALL fileStreamOpen
BNQ openFailed
CALL fullPage
nextBlock:
CALL fileStreamNext
BNQ readFailed
PSHD.3
POPB
POPA
SETD.2 Remaining
STA.2
INCD.2
STB.2
OR
BRQ finished
SETD.1 FileStreamBlock
SETD.2 Remaining
printLoop:
LDA.1
OUTA 0x00
INIB 0x0A
XOR
BNQ bytePrinted
SETD.3 LinesLeft
LDA.3
DECA
STA.3
BNA bytePrinted
CALL pause
BNQ finished
bytePrinted:
INCD.1
CALL fileStreamTakeRemaining
BRQ nextBlock
BRI printLoop
; Ask for one key without asking for Return. The console is put back immediately,
; including on q and end of redirected input.
pause:
SETD.0 MorePrompt
SWI osPrintString
INIA 0x01
OUTA 0x02
INA 0x00
PSHA ; Keep the key while A is used to restore line mode.
RSTA
OUTA 0x02
POPA
SETD.0 ClearPrompt
SWI osPrintString
INIB 0x71 ; q
XOR
BRQ pauseQuit
INIB 0xFF ; end of redirected input
XOR
BRQ pauseQuit
INIB 0x0A ; Return: one more line.
XOR
BRQ oneLine
INIB 0x20 ; Space: one more screen.
XOR
BNQ pause ; Ignore every other key.
CALL fullPage
RSTA
RSTB
OR
RET
oneLine:
SETD.3 LinesLeft
INIA 0x01
STA.3
RSTA
RSTB
OR
RET
pauseQuit:
RSTA
INIB 0x01
OR
RET
fullPage:
SETD.3 LinesLeft
INIA 0d22
STA.3
RET
noName:
SETD.0 Usage
SWI osPrintString
INIA 0d2
SWI osExit
openFailed:
SETD.0 OpenError
SWI osPrintString
BRI printError
readFailed:
SETD.0 ReadError
SWI osPrintString
printError:
RSTA
MVQB
SWI osPrintNumber
SETD.0 NewLine
SWI osPrintString
; ITS OWN EXIT, and it did not have one. This fell through into finished and reported
; that everything was fine, having just printed the reason it was not - which nobody
; noticed while the only reader was a person, who could see both.
INIA 0d1
SWI osExit
finished:
RSTA
SWI osExit
#Data
#Base 0x2000
Usage:
"more: give me a file name
"
OpenError:
"more: cannot find the file, error "
ReadError:
"more: cannot read the file, error "
NewLine:
0x0A 0x00
MorePrompt:
"-- more --"
ClearPrompt:
0x0A 0x00
Name:
#Reserve 0d29
Remaining:
0x00 0x00
LinesLeft:
0x00
#Include fileStream.asm
+138
View File
@@ -0,0 +1,138 @@
; Once.asm
; Starts something else next time, and only next time.
;
; > Once /System/Boot/mine.bin
; next start: /System/Boot/mine.bin, once
; > reboot
;
; Writes /System/Boot/once.cfg, which the loader reads before boot.cfg and DELETES BEFORE
; IT JUMPS. So the image runs on the next start and on no other, whatever happens to it -
; a one shot that hangs cannot hang twice, because the request is gone before it ran.
;
; ---- What this is for ----
;
; A program that owns the whole machine has nowhere to run. It cannot be started from the
; shell, because starting it means there is no shell; and pointing boot.cfg at it means a
; machine that keeps starting it, which is a poor place to find a mistake. This is the
; missing step: write it, ask for it once, and the system comes back by itself.
;
; The file is the same format as boot.cfg because a second format for one setting would be
; a second format. It says `system` for the same reason.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Wanted
INIB 0d128
SWI osArgument
MVQA
BNA noName
SETD.0 Wanted
LDA.0
BRA noName
; The line, built as "system " and then the name. One write, because the file is the
; whole of the request and half of it would be a request for half a thing.
SETD.0 Prefix
SETD.1 Line
RCAL copyString
SETD.0 Wanted
RCAL copyString
INIA 0x0A
STA.1
INCD.1
RSTA
STA.1
; How long it came to, which is what the write is told.
SETD.0 Line
RCAL measure
SETD.0 OncePath
SETD.1 Line
SWI osFileSave
MVQA
BNA noWrite
SETD.0 DoneText
SWI osPrintString
SETD.0 Wanted
SWI osPrintString
SETD.0 OnceText
SWI osPrintString
RSTA
SWI osExit
noName:
SETD.0 Usage
SWI osPrintString
INIA 0d2
SWI osExit
noWrite:
SETD.0 NoWriteText
SWI osPrintString
INIA 0d1
SWI osExit
; DP0 names a string and DP1 where it goes. DP1 is left on the zero at the end, so one
; string can be written straight after another.
copyString:
LDA.0
BRA copyDone
STA.1
INCD.0
INCD.1
BRI copyString
copyDone:
RRET
; DP0 names the line. osFileSave wants a size, and a file of whole blocks and a tail is
; that count with the blocks in A and the tail in B - one block is never full here.
measure:
RSTB
measureLoop:
LDA.0
BRA measureDone
INCD.0
MVQB
INIA 0d1
CCF
ADD
MVQB
BRI measureLoop
measureDone:
RSTA
RRET
#Data
#Base 0x2000
Prefix:
"system "
Usage:
"once what? try: Once /System/Boot/something.bin
"
DoneText:
"next start: "
OnceText:
", once
"
NoWriteText:
"it would not write
"
OncePath:
"/System/Boot/once.cfg"
Wanted:
#Reserve 0d129
Line:
#Reserve 0d160
+245
View File
@@ -0,0 +1,245 @@
; Writes a file one block at a time, without ever holding the whole of it.
;
; This is the write side of what Stream demonstrates for reading: a file bigger than the
; memory building it. It writes as many blocks as it is asked for, each one filled with a
; pattern that says which block it is, so that what comes off the disk afterwards can be
; checked against what should have gone on rather than merely being the right length.
;
; The argument is how many blocks, in decimal. The last one is deliberately a part block,
; because a tail is the case every off-by-one in a filesystem hides in.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Argument
INIB 0d8
SWI osArgument
SETD.0 Argument
LDA.0
BRA useDefault
CALL readCount
BRI counted
useDefault:
INIA 0d4
SETD.0 Blocks
STA.0
counted:
; Nothing may be zero blocks: the tail below would then be the whole file.
SETD.0 Blocks
LDA.0
BNA haveCount
INIA 0d1
STA.0
haveCount:
; Whole blocks, and a tail of forty bytes on the end of them.
SETD.0 Name
SETD.3 0x00 0x00
SETD.0 Blocks
LDA.0
PSHA
POPB
RSTA
PSHA
PSHB
POPD.3
SETD.0 Name
INIA 0d40
SWI osFileStart
BNQ startFailed
; Each whole block, filled with its own number.
RSTA
SETD.0 Which
STA.0
nextBlock:
SETD.0 Which
LDA.0
SETD.2 Blocks
LDB.2
CCF
SUB
BRQ theTail
CALL fillBlock
SETD.1 Block
SETD.0 Which
LDA.0
RSTB
PSHA
POPB
RSTA
SWI osFileWrite
BNQ writeFailed
SETD.0 Which
LDA.0
INCA
STA.0
BRI nextBlock
theTail:
; And the part block at the end, which is the same fill cut short by the size given at
; the start. Only the first forty bytes of it will belong to the file.
CALL fillBlock
SETD.1 Block
SETD.0 Which
LDA.0
RSTB
PSHA
POPB
RSTA
SWI osFileWrite
BNQ writeFailed
; And how big it turned out to be, which here is what was asked for: this one knows its
; size from the start. Something that did not - an assembler, say - would ask for more
; than it needed and say the truth here.
RSTA
PSHA
SETD.0 Blocks
LDA.0
PSHA
POPD.3
INIA 0d40
SWI osFileDone
BNQ doneFailed
SETD.0 Wrote
SWI osPrintString
SETD.0 Blocks
LDB.0
RSTA ; A and B together are the number, so the count is the low half.
SWI osPrintNumber
SETD.0 AndTail
SWI osPrintString
RSTA
SWI osExit
; The block becomes 256 copies of the block number plus a fixed byte, so that a block
; written into the wrong place is visible rather than merely being bytes.
fillBlock:
SETD.0 Block
SETD.1 Which
LDA.1
INIB 0x41
CCF
ADD
MVQA
RSTB
fillLoop:
STA.0
INCD.0
DECB
BNB fillLoop
RET
; The argument, in decimal, into Blocks. Anything that is not a digit ends it.
readCount:
RSTA
SETD.1 Blocks
STA.1
SETD.0 Argument
countLoop:
LDA.0
BRA countDone
INIB 0x30
CCF
SUB
MVQA
INIB 0d10
CCF
SUB
BNC countDone ; Not a digit, so the number ended.
SETD.1 Blocks
LDB.1
PSHA
INIA 0d10
CALL timesTen
POPA
SETD.1 Scratch
LDB.1
CCF
ADD
MVQA
SETD.1 Blocks
STA.1
INCD.0
BRI countLoop
countDone:
RET
; B times ten into Scratch, by adding it up. Nothing here is bigger than a byte.
timesTen:
RSTA
SETD.0 Scratch
STA.0
INIA 0d10
tenLoop:
PSHA
SETD.0 Scratch
LDA.0
SETD.2 TenHold
STB.2
LDB.2
CCF
ADD
MVQA
STA.0
POPA
DECA
BNA tenLoop
RET
startFailed:
SETD.0 NoStart
SWI osPrintString
INIA 0d1
SWI osExit
writeFailed:
SETD.0 NoWrite
SWI osPrintString
INIA 0d1
SWI osExit
doneFailed:
SETD.0 NoDone
SWI osPrintString
INIA 0d1
SWI osExit
#Data
#Base 0x2000
Name:
"poured.dat"
Argument:
#Reserve 0d9
Blocks:
0x00
Which:
0x00
Scratch:
0x00
TenHold:
0x00
Block:
#Reserve 0d256
Wrote:
"poured "
AndTail:
" blocks and a tail of 40
"
NoStart:
"could not start it
"
NoWrite:
"could not write a block
"
NoDone:
"could not finish it
"
+52
View File
@@ -0,0 +1,52 @@
; Reboot.asm
; Starts the machine again.
;
; Whatever put the first instruction in memory does it again, and everything after that
; follows: the boot slot is read, the loader runs, and whatever the configuration names -
; or whatever Once asked for - is what starts.
;
; ---- Why this writes a port ----
;
; A reset has to work when the system does not. Something that could only be asked for
; through SWI would be unavailable in exactly the case that wants it most, and a program
; that owns the whole machine has no system to ask. So the machine takes it directly, the
; way it takes everything else: a byte out of a port.
;
; It is still a program rather than a shell word, because it is not the shell's business
; and because a word built into the shell is not callable by anything else.
;
; ---- What survives ----
;
; The disk, and everything written to it. That is what warm means: the machine starts
; again, the world it starts into does not. Nothing here flushes anything, because nothing
; on this machine is held back - a file is on the disk when the write returns.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Saying
SWI osPrintString
INIA 0x01
OUTA 0x13
; Not reached. The machine has started over by the time the next instruction would run,
; so anything here is a statement about a machine that no longer exists - but a program
; whose last instruction is an output is one byte from running into whatever follows it,
; and that is not a habit worth keeping.
RSTA
SWI osExit
#Data
#Base 0x2000
Saying:
"starting again
"
+3 -2
View File
@@ -16,7 +16,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
SETD.0 Given SETD.0 Given
@@ -40,11 +40,12 @@ sayNothing:
sayEnd: sayEnd:
SETD.0 NewLine SETD.0 NewLine
SWI osPrintString SWI osPrintString
RSTA
SWI osExit SWI osExit
#Data #Data
#Base 0x1000 #Base 0x2000
SaidText: SaidText:
"it says: " "it says: "
+93
View File
@@ -0,0 +1,93 @@
; Settle.asm
; Says how the last start went, and tells the machine to stop falling back.
;
; A program rather than a shell command, because the shell is for the things you cannot do
; without it and this is not one of them. 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 sequence - and none of that is true of a word built into the shell.
;
; ---- What settling means ----
;
; The loader marks the disk before handing over and the system clears the mark on reaching
; its prompt, so a mark still set is a start that never arrived. After that the loader uses
; the fallback and KEEPS USING IT, because a system known not to start should not be tried
; every other boot for ever.
;
; Settling is how it is told that has changed. It does not fix anything and it does not
; check anything: it says "the situation is different now, try again". Which is why it is a
; deliberate act by somebody who has just changed something, rather than anything automatic.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
SWI osBootState
MVQA
SETD.0 State
STA.0
BRA alreadySettled
INIB 0d2
XOR
BRQ fellBack
; Trying. Nothing has gone wrong yet - this is what the disk looks like while a start is
; still in progress, which from in here means the system that is running has not reached
; its prompt, which it plainly has. So the mark is stale.
SETD.0 WasTrying
SWI osPrintString
BRI doSettle
fellBack:
SETD.0 WasFallen
SWI osPrintString
doSettle:
SWI osBootSettle
MVQA
BNA settleFailed
SETD.0 Settled
SWI osPrintString
RSTA
SWI osExit
settleFailed:
SETD.0 NoDisk
SWI osPrintString
INIA 0d1
SWI osExit
alreadySettled:
SETD.0 Already
SWI osPrintString
RSTA
SWI osExit
#Data
#Base 0x2000
WasTrying:
"the disk says a start is still in progress
"
WasFallen:
"the disk says the last start did not arrive, so this is the fallback
"
Settled:
"settled: the next start will use the configuration again
"
Already:
"already settled: the next start will use the configuration
"
NoDisk:
"nothing to settle: no disk answered
"
State:
0x00
+3 -2
View File
@@ -9,7 +9,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
RSTA RSTA
@@ -74,6 +74,7 @@ advancePage:
finished: finished:
CALL lineFeed CALL lineFeed
RSTA
SWI osExit ; Return to CosmOS. SWI osExit ; Return to CosmOS.
; DP0 points at a PrimeStates entry. CALL restores it on return. ; DP0 points at a PrimeStates entry. CALL restores it on return.
@@ -139,7 +140,7 @@ printCandidateHex:
#Data #Data
#Base 0x1000 #Base 0x2000
; Segment has to begin on a page boundary, and now says so itself rather than relying on ; Segment has to begin on a page boundary, and now says so itself rather than relying on
; whatever happens to have been assembled before it. The marking loop adds the prime to ; whatever happens to have been assembled before it. The marking loop adds the prime to
+3 -2
View File
@@ -4,7 +4,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
; Search the list until we find a prime. ; Search the list until we find a prime.
@@ -38,6 +38,7 @@ start:
BRI markMultiples ; Otherwise, loop again to mark the next multiple as nonprime. BRI markMultiples ; Otherwise, loop again to mark the next multiple as nonprime.
end: end:
CALL lineFeed ; Print a linefeed to make it look nice. CALL lineFeed ; Print a linefeed to make it look nice.
RSTA
SWI osExit ; The program is done, we found all the primes! SWI osExit ; The program is done, we found all the primes!
@@ -45,7 +46,7 @@ start:
#Data #Data
#Base 0x1000 #Base 0x2000
; The table of our prime candidates. It has to begin on a page boundary: marking walks ; The table of our prime candidates. It has to begin on a page boundary: marking walks
; the pointer's low byte and treats the carry out as running off the end of the table, ; the pointer's low byte and treats the carry out as running off the end of the table,
+3 -5
View File
@@ -38,7 +38,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
; The two pointers whose low byte is a square number. Both regions are page aligned, so ; The two pointers whose low byte is a square number. Both regions are page aligned, so
@@ -105,6 +105,7 @@ gameOverSay:
CALL newLine CALL newLine
RSTA RSTA
OUTA 0x02 ; Line mode, the way it was found. OUTA 0x02 ; Line mode, the way it was found.
RSTA ; splitlint[redundant-assignment]: an exit status, not a console mode
SWI osExit SWI osExit
; ---- Reaching a square ---- ; ---- Reaching a square ----
@@ -334,7 +335,6 @@ takeKeyTurn:
XOR XOR
POPA POPA
BRQ takeKeyDone ; Opposite, so it is not a turn anybody can make. BRQ takeKeyDone ; Opposite, so it is not a turn anybody can make.
SETD.0 Direction
STA.0 STA.0
RET RET
@@ -430,7 +430,6 @@ stepDown:
INIB 0xF0 INIB 0xF0
AND AND
MVQA MVQA
INIB 0xF0
XOR XOR
POPA POPA
BRQ stepWall ; The bottom row is where the high nibble is fifteen. BRQ stepWall ; The bottom row is where the high nibble is fifteen.
@@ -453,7 +452,6 @@ stepRight:
INIB 0x0F INIB 0x0F
AND AND
MVQA MVQA
INIB 0x0F
XOR XOR
POPA POPA
BRQ stepWall BRQ stepWall
@@ -605,7 +603,7 @@ pauseInner:
#Data #Data
#Base 0x1000 #Base 0x2000
HeadCell: HeadCell:
0x00 0x00
+87
View File
@@ -0,0 +1,87 @@
; Status.asm
; Says what the last program made of what it was asked to do.
;
; The shell keeps the number and does not print it, because a program that failed has
; already said so in words and a number beside that would be noise. But a number nobody can
; see is a number nobody can trust, so this is how a person looks.
;
; 0 it did what it was asked
; 1 it did not
; 2 it was asked wrongly
;
; A program may give its own meanings if it says so, and Compare does: one there means the
; files differ, which is a result rather than a failure.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
SWI osLastStatus
MVQA
SETD.0 Was
STA.0
SETD.0 Prefix
SWI osPrintString
; A and B together, most significant first - so the status is the LOW half. Put in A the
; first time this was written, which printed a status of two as five hundred and twelve.
RSTA
SETD.0 Was
LDB.0
SWI osPrintNumber
; And in words, for the three the system itself uses.
SETD.0 Was
LDA.0
BRA sayWorked
INIB 0d1
XOR
BRQ sayFailed
SETD.0 Was
LDA.0
INIB 0d2
XOR
BRQ sayAsked
BRI done
sayWorked:
SETD.0 WorkedText
SWI osPrintString
BRI done
sayFailed:
SETD.0 FailedText
SWI osPrintString
BRI done
sayAsked:
SETD.0 AskedText
SWI osPrintString
done:
SETD.0 NewLine
SWI osPrintString
RSTA
SWI osExit
#Data
#Base 0x2000
Prefix:
"the last program left "
WorkedText:
", which is: it did what it was asked"
FailedText:
", which is: it did not"
AskedText:
", which is: it was asked wrongly"
NewLine:
"
"
Was:
0x00
+9 -5
View File
@@ -36,7 +36,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
; ---- 1. How big is something that will not fit ---- ; ---- 1. How big is something that will not fit ----
@@ -216,6 +216,7 @@ missing:
SWI osPrintString SWI osPrintString
CALL printWhy CALL printWhy
INIA 0d1
SWI osExit SWI osExit
noBig: noBig:
@@ -223,14 +224,17 @@ noBig:
SETD.0 NoBigText SETD.0 NoBigText
SWI osPrintString SWI osPrintString
CALL printWhy CALL printWhy
INIA 0d1
SWI osExit SWI osExit
noSmall: noSmall:
SETD.0 NoSmallText SETD.0 NoSmallText
SWI osPrintString SWI osPrintString
INIA 0d1
SWI osExit SWI osExit
noRename: noRename:
SETD.0 NoRenameText SETD.0 NoRenameText
SWI osPrintString SWI osPrintString
INIA 0d1
SWI osExit SWI osExit
; ---- Routines ---- ; ---- Routines ----
@@ -290,11 +294,11 @@ checksumLoop:
INCD.2 INCD.2
LDA.2 LDA.2
BNA checksumLow BNA checksumLow
DPDN.2 0d01 DECD.2
LDA.2 LDA.2
DECA DECA
STA.2 ; Borrow out of the high byte. STA.2 ; Borrow out of the high byte.
DPUP.2 0d01 INCD.2
INIA 0xFF INIA 0xFF
STA.2 STA.2
BRI checksumTest BRI checksumTest
@@ -333,7 +337,7 @@ stepIndex:
INCA INCA
STA.2 STA.2
BNC stepIndexDone BNC stepIndexDone
DPDN.2 0d01 DECD.2
LDA.2 LDA.2
INCA INCA
STA.2 STA.2
@@ -404,7 +408,7 @@ printWhy:
#Data #Data
#Base 0x1000 #Base 0x2000
BigName: BigName:
"big.txt" "big.txt"
+88
View File
@@ -0,0 +1,88 @@
; Print a text file without asking it to fit in Data Memory.
; fileStream.asm owns finding the file and walking its blocks. Type owns only what makes
; it Type: send each byte to the console until the stream says there are no more.
;
; Written by ChatGPT for Anachronaut's SplitBit
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Name
INIB 0d29
SWI osArgument
SETD.0 Name
LDA.0
BRA noName
CALL fileStreamOpen
BNQ openFailed
nextBlock:
CALL fileStreamNext
BNQ readFailed
PSHD.3
POPB
POPA
SETD.2 Remaining
STA.2
INCD.2
STB.2
OR
BRQ finished
SETD.1 FileStreamBlock
SETD.2 Remaining
printLoop:
LDA.1
OUTA 0x00
INCD.1
CALL fileStreamTakeRemaining
BRQ nextBlock
BRI printLoop
noName:
SETD.0 Usage
SWI osPrintString
INIA 0d2
SWI osExit
openFailed:
SETD.0 OpenError
SWI osPrintString
BRI printError
readFailed:
SETD.0 ReadError
SWI osPrintString
printError:
RSTA
MVQB
SWI osPrintNumber
SETD.0 NewLine
SWI osPrintString
; ITS OWN EXIT, and it did not have one. This fell through into finished and reported
; that everything was fine, having just printed the reason it was not - which nobody
; noticed while the only reader was a person, who could see both.
INIA 0d1
SWI osExit
finished:
RSTA
SWI osExit
#Data
#Base 0x2000
Usage:
"type: give me a file name
"
OpenError:
"type: cannot find the file, error "
ReadError:
"type: cannot read the file, error "
NewLine:
0x0A 0x00
Name:
#Reserve 0d29
Remaining:
0x00 0x00
#Include fileStream.asm
+109
View File
@@ -0,0 +1,109 @@
; Goes somewhere else and reads a file by a bare name once it is there.
;
; This exists to prove two things the shell promises and nothing else could test. First
; that osChangeDir works: the file it prints is named with no path at all, so the only way
; to reach it is to be standing in the right place. Second that the shell puts the working
; directory back afterwards: the prompt after this returns says where the shell was, not
; where this went.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
; Where to go is the argument. Nothing else about this program says a directory name, so
; running it anywhere else moves it anywhere else.
SETD.0 Where
INIB 0d40
SWI osArgument
SETD.0 Where
LDA.0
BRA noWhere
SETD.0 Where
SWI osChangeDir
BNQ noSuchPlace
SETD.0 Went
SWI osPrintString
; And now a bare name, which means nothing until you are somewhere.
SETD.0 Bare
CALL fileStreamOpen
BNQ noFile
wanderBlock:
CALL fileStreamNext
BNQ noFile
PSHD.3
POPB
POPA
SETD.2 Left
STA.2
INCD.2
STB.2
SETD.2 Left
LDA.2
INCD.2
LDB.2
OR
BRQ wanderDone
SETD.0 FileStreamBlock
SETD.2 Left
INCD.2
LDB.2
wanderByte:
LDA.0
OUTA 0x00
INCD.0
DECB
BNB wanderByte
BRI wanderBlock
wanderDone:
RSTA
SWI osExit
noWhere:
SETD.0 NoWhereText
SWI osPrintString
INIA 0d1
SWI osExit
noSuchPlace:
SETD.0 NoPlaceText
SWI osPrintString
INIA 0d1
SWI osExit
noFile:
SETD.0 NoFileText
SWI osPrintString
INIA 0d1
SWI osExit
#Data
#Base 0x2000
Where:
#Reserve 0d40
Left:
0x00 0x00
Went:
"moved, and reading a bare name from there:
"
NoWhereText:
"wander where?
"
NoPlaceText:
"cannot go there
"
NoFileText:
"nothing of that name here
"
Bare:
"notes.txt"
#Include fileStream.asm
+3 -2
View File
@@ -17,7 +17,7 @@
#Program #Program
#Base 0x2000 ; Above the system, which keeps below here. #Base 0x4000 ; Above the system, which keeps below here.
greet: greet:
SETD.0 Opening SETD.0 Opening
@@ -39,11 +39,12 @@ greet:
; Give the machine back. The system takes its Stack back at this point, so everything ; Give the machine back. The system takes its Stack back at this point, so everything
; this program pushed goes with it. ; this program pushed goes with it.
RSTA
SWI osExit SWI osExit
#Data #Data
#Base 0x1000 ; And its data above the system's. #Base 0x2000 ; And its data above the system's.
Opening: Opening:
"a program, loaded off a disk, running on the system that loaded it "a program, loaded off a disk, running on the system that loaded it
+3 -2
View File
@@ -6,7 +6,7 @@
#Program #Program
#Base 0x2000 ; Change two: #Base 0x4000 ; Change two:
SETD hello ; Change three SETD hello ; Change three
Start: Start:
@@ -21,10 +21,11 @@ End:
OUTA 0x00 ; Output it to the text console. OUTA 0x00 ; Output it to the text console.
;HALT ; Terminate the program. ;HALT ; Terminate the program.
; Instead, let's call osExit to return the system nicely. Fourth change. ; Instead, let's call osExit to return the system nicely. Fourth change.
RSTA
SWI osExit SWI osExit
#Data #Data
#Base 0x1000 ; Five, adjust the base of the data segment. #Base 0x2000 ; Five, adjust the base of the data segment.
hello: ; Throw a label here so we can explicitly point at this data. Six, actually. hello: ; Throw a label here so we can explicitly point at this data. Six, actually.
"Hello, World!" "Hello, World!"
+341 -41
View File
@@ -38,7 +38,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
SETD.0 Argument SETD.0 Argument
@@ -57,6 +57,7 @@ start:
CALL settleFormat CALL settleFormat
CALL deriveName ; After the first pass: what it is called depends on whether a CALL deriveName ; After the first pass: what it is called depends on whether a
; #Base turned up, and that is not known until then. ; #Base turned up, and that is not known until then.
CALL layOutImage CALL layOutImage
BNQ stopped BNQ stopped
CALL passTwo CALL passTwo
@@ -202,7 +203,7 @@ passNotVectorName:
BNQ passNotSwi BNQ passNotSwi
SETD.0 ClsOpcode SETD.0 ClsOpcode
LDA.0 LDA.0
INIB 0x18 INIB 0x72 ; SWI. Moved with the rest of the subroutine block.
XOR XOR
BNQ passNotSwi BNQ passNotSwi
INIA 0d1 INIA 0d1
@@ -735,7 +736,7 @@ emitStringLoop:
CALL numStep CALL numStep
SETD.1 EmitWalk SETD.1 EmitWalk
LDD.0.1 LDD.0.1
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
BNA emitStringLoop ; The zero goes out with the rest and then stops the loop. BNA emitStringLoop ; The zero goes out with the rest and then stops the loop.
BRI emitDone BRI emitDone
@@ -1276,7 +1277,6 @@ dropColon:
LDA.0 LDA.0
BRA dropColonDone BRA dropColonDone
DECA DECA
SETD.0 TokLength
STA.0 STA.0
SETD.0 TokText SETD.0 TokText
SETD.1 DropWalk SETD.1 DropWalk
@@ -1360,8 +1360,7 @@ layOutImage:
BNQ layOutNo BNQ layOutNo
SETD.0 ImgWalk SETD.0 ImgWalk
SETD.2 ScratchImage CALL numZero ; The front of the file, not the front of a buffer.
CALL numSet
SETD.0 MagicSPBT SETD.0 MagicSPBT
INIA 0d4 INIA 0d4
CALL putBytes CALL putBytes
@@ -1414,8 +1413,7 @@ layOutLoadable:
BNQ layOutNo BNQ layOutNo
SETD.0 ImgWalk SETD.0 ImgWalk
SETD.2 ScratchImage CALL numZero ; The front of the file, not the front of a buffer.
CALL numSet
SETD.0 MagicSBEX SETD.0 MagicSBEX
INIA 0d4 INIA 0d4
CALL putBytes CALL putBytes
@@ -1460,17 +1458,113 @@ layOutNo:
ADD ADD
RET RET
; ---- Room for the file ----
;
; Nothing is held in memory any more, so what is checked here is not a buffer. It is that
; the SIZE CAN STILL BE DESCRIBED: every number in this assembler is two bytes, so a file
; of more than 65,535 would have wrapped round while it was being added up and come out
; smaller than one of its own segments. A wrapped total would allocate a short file and
; write off the end of it.
;
; Then the file is started, which is where running out of DISK is found - and that happens
; before a single byte is written, so a refusal costs nothing that was already there.
checkImageRoom: checkImageRoom:
SETD.0 ImgRoom SETD.0 ImgTotal
SETD.2 ImgTotal SETD.2 ImgProgLen
CALL numCompare
BRC imageTooBig ; The total is smaller than a part of it: it went round.
SETD.0 ImgTotal
SETD.2 ImgDataLen
CALL numCompare CALL numCompare
BRC imageTooBig BRC imageTooBig
; ---- How much room to ask for ----
;
; MORE THAN THE FILE WILL COME TO, on purpose. How many vectors are actually installed is
; not known until the second pass has resolved every handler, and by then the file has to
; exist to be written into. So the room asked for is the whole file plus four bytes for
; every vector the table can hold, and five for a marker.
;
; THE LIMIT RATHER THAN THE COUNT SO FAR, and that distinction cost a real bug. Asking
; for four bytes per vector DECLARED looks like a safe bound and is not: a device is
; declared during the SECOND pass, in the line that implements it, so a program with one
; 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 -
; which happened to land inside the last block it owned, and would not have if the
; boundary had fallen four bytes earlier.
;
; The limit cannot go stale that way. It is what the vector table holds, so no assembly
; can install more, whenever they are counted.
;
; Asking for too much costs nothing but a moment: osFileDone is told what it really came
; to and the difference goes back. Asking for too little would have meant writing off the
; end of the file, and the first sign of it was Keys coming out four bytes short.
SETD.0 ImgAsk
SETD.2 ImgTotal
CALL numSet
SETD.0 ImgVecMost
SETD.2 VecLimit
CALL numSet
SETD.0 ImgVecMost
SETD.2 ImgVecMost
CALL numAdd
SETD.0 ImgVecMost
SETD.2 ImgVecMost
CALL numAdd ; Four bytes an entry.
SETD.0 ImgAsk
SETD.2 ImgVecMost
CALL numAdd
INIA 0d5
SETD.0 ImgAsk
CALL numAddByte
; Blocks are the high half of the size and the tail is the low half, which is how an
; entry holds one and why nothing here has to divide.
SETD.1 ImgAsk
LDA.1
SETD.0 ImgOutBlocks
STA.0 ; The high half is whole blocks.
INCD.1
LDA.1
SETD.0 ImgOutTail
STA.0 ; And the low half is what is left over.
; DP3 wants the block count and A the tail. The Stack takes the high byte first, which is
; the way every reader in the system takes a count back out of DP3.
RSTA
PSHA
SETD.0 ImgOutBlocks
LDA.0
PSHA
POPD.3
SETD.0 ImgOutTail
LDA.0
SETD.0 OutName
SWI osFileStart
BNQ imageNoRoom
; Nothing in the window yet.
RSTA
SETD.0 OutHeld
STA.0
SETD.0 OutDirty
STA.0
RSTA RSTA
RSTB RSTB
CCF CCF
ADD ADD
RET RET
imageNoRoom:
SETD.0 NoRoomText
SWI osPrintString
RSTA
INIB 0d1
CCF
ADD
RET
imageTooBig: imageTooBig:
SETD.0 TooBigText SETD.0 TooBigText
SWI osPrintString SWI osPrintString
@@ -1481,16 +1575,171 @@ imageTooBig:
RET RET
; Puts A down at ImgWalk and steps it. ; Puts A down at ImgWalk and steps it.
putByte: ; ---- Putting a byte into the file ----
;
; THE FILE IS WRITTEN AS IT IS MADE, through one block of window, rather than being built
; whole in memory and handed over at the end. Everything below works in FILE OFFSETS: a
; cursor is a two byte number counting from the front of the file, and because 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. No division anywhere, which is just as well.
;
; There are two cursors and they move independently, because #Program and #Data alternate
; in source and the two segments are consecutive in the file. Whenever a byte lands in a
; block other than the one in hand, the one in hand goes back to the disk and the new one
; is fetched - so a run of bytes in one segment costs nothing extra, and a switch between
; segments costs two block operations. There are a few dozen switches in a source file and
; several thousand bytes.
;
; The block is FETCHED rather than assumed blank, and that is what makes the whole thing
; work with one window instead of three. The header is patched after every byte is out; the
; block where the program ends is the same block the data begins in; both are simply
; revisits, and a revisit is what fetching handles.
; A at the file offset in the two byte number DP2 names, which is then stepped on.
putAt:
SETD.0 ImgHold SETD.0 ImgHold
STA.0 STA.0
SETD.1 ImgWalk
LDD.0.1 ; WHICH CURSOR THIS IS, KEPT IN MEMORY. It arrives in DP2 and everything below wants DP2
SETD.2 ImgHold ; for something of its own - and a call puts DP2 back the way it was AT THE CALL, not the
LDA.2 ; way it was on the way in. Leaving it there meant the step at the end moved whatever the
STA.0 ; last call had left in DP2, which was the address of the window, and the window walked
; off across memory while the cursor stood still.
SETD.0 ImgCursor
STD.2.0
; Which block, and where in it: a block is two hundred and fifty six bytes, so the two
; halves of the offset are exactly those two things.
PSHD.2
POPD.0
LDA.0
SETD.1 ImgBlockWant
STA.1
INCD.0 INCD.0
STD.0.1 LDA.0
SETD.1 ImgSlot
STA.1
CALL outHold
BNQ putAtFailed
SETD.0 OutAt
SETD.2 ScratchWindow
CALL numSet
SETD.0 OutAt
SETD.1 ImgSlot
LDA.1
CALL numAddByte
SETD.1 OutAt
LDD.0.1
SETD.1 ImgHold
LDA.1
STA.0
INIA 0x01
SETD.0 OutDirty
STA.0
SETD.1 ImgCursor
LDD.0.1
CALL numStep
RSTA
RSTB
CCF
ADD
RET
putAtFailed:
RSTA
INIB 0d1
CCF
ADD
RET
; Makes sure the block ImgBlockWant names is the one in the window, writing back whatever
; was there and fetching the new one.
outHold:
SETD.0 OutHeld
LDA.0
BRA outFetch ; Nothing in hand at all.
SETD.0 OutBlock
LDA.0
SETD.2 ImgBlockWant
LDB.2
CCF
SUB
BRQ outHeldAlready
CALL outFlush
BNQ outHoldNo
outFetch:
SETD.0 ImgBlockWant
LDA.0
SETD.1 OutBlock
STA.1
SETD.1 ScratchWindow
LDD.1.1
SETD.0 OutBlock
LDB.0
RSTA
SWI osFileFetch
BNQ outHoldNo
INIA 0x01
SETD.0 OutHeld
STA.0
RSTA
SETD.0 OutDirty
STA.0
outHeldAlready:
RSTA
RSTB
CCF
ADD
RET
outHoldNo:
RSTA
INIB 0d1
CCF
ADD
RET
; Whatever is in the window goes back to the file, if anything has been put in it.
outFlush:
SETD.0 OutHeld
LDA.0
BRA outFlushNothing
SETD.0 OutDirty
LDA.0
BRA outFlushNothing
SETD.1 ScratchWindow
LDD.1.1
SETD.0 OutBlock
LDB.0
RSTA
SWI osFileWrite
BNQ outHoldNo
RSTA
SETD.0 OutDirty
STA.0
outFlushNothing:
RSTA
RSTB
CCF
ADD
RET
; The header and everything else that is written in order goes through ImgWalk.
putByte:
SETD.2 ImgWalk
CALL putAt
RET RET
; Puts A bytes from DP0 down at ImgWalk. ; Puts A bytes from DP0 down at ImgWalk.
@@ -1543,12 +1792,11 @@ emitByte:
emitToProgram: emitToProgram:
SETD.1 ProgPut SETD.1 ProgPut
emitPut: emitPut:
LDD.0.1 PSHD.1
SETD.2 EmitHold POPD.2 ; ProgPut or DataPut, whichever this byte belongs to.
LDA.2 SETD.0 EmitHold
STA.0 LDA.0
INCD.0 CALL putAt
STD.0.1
RET RET
; The byte at DP0 offset by A, into ClsByte. The classifier has one of these; this is the ; The byte at DP0 offset by A, into ClsByte. The classifier has one of these; this is the
@@ -1676,8 +1924,8 @@ writeVectorsHeader:
LDA.0 LDA.0
BRA writeVectorsNone BRA writeVectorsNone
SETD.0 ImgWalk SETD.0 ImgWalk
SETD.2 ScratchImage CALL numZero ; Back to the front of the file, which is a revisit like
CALL numSet ; any other: the window fetches block nought again.
SETD.0 ImgWalk SETD.0 ImgWalk
INIA 0d4 INIA 0d4
CALL numAddByte CALL numAddByte
@@ -1695,8 +1943,8 @@ writeVectorsNone:
LDA.0 LDA.0
BRA writeVectorsOut BRA writeVectorsOut
SETD.0 ImgWalk SETD.0 ImgWalk
SETD.2 ScratchImage CALL numZero ; Back to the front of the file, which is a revisit like
CALL numSet ; any other: the window fetches block nought again.
SETD.0 ImgWalk SETD.0 ImgWalk
INIA 0d8 INIA 0d8
CALL numAddByte CALL numAddByte
@@ -1772,15 +2020,32 @@ vectorNotEntry:
ADD ADD
RET RET
; Whatever is still in the window, and then the file takes its name. Nothing that was
; already on the disk has been touched until this last step.
writeImage: writeImage:
SETD.0 OutName CALL outFlush
SETD.1 ScratchImage BNQ writeFailed
LDD.1.1
SETD.2 ImgTotal ; What it really came to, which is what the room was asked for less whatever the vectors
LDA.2 ; did not need.
INCD.2 SETD.1 ImgTotal
LDB.2 LDA.1
SWI osFileSave SETD.0 ImgOutBlocks
STA.0
INCD.1
LDA.1
SETD.0 ImgOutTail
STA.0
RSTA
PSHA
SETD.0 ImgOutBlocks
LDA.0
PSHA
POPD.3
SETD.0 ImgOutTail
LDA.0
SWI osFileDone
BNQ writeFailed BNQ writeFailed
RSTA RSTA
RSTB RSTB
@@ -1915,7 +2180,7 @@ report:
#Data #Data
#Base 0x1000 #Base 0x2000
Argument: Argument:
#Reserve 0d23 #Reserve 0d23
@@ -2047,11 +2312,46 @@ ImgHold:
DropWalk: DropWalk:
0x00 0x00 0x00 0x00
; How big an output file this can build. Everything the assembler makes has to fit here at once, ; ---- The output, which goes out as it is made ----
; because a file is written in one call and there is nowhere to put half of one. CosmOS ;
; itself comes to 9,564 bytes. ; THERE IS NO LIMIT ON HOW BIG A FILE THIS CAN BUILD, and that is the point of the window
ImgRoom: ; below. The assembler used to hold the whole output in an eighteen kilobyte buffer and
0x34 0x00 ; write it in one call at the end, which made the largest program it could assemble a
; property of ITS OWN memory rather than of the disk - and cosmos.bin reached 13,245 bytes
; of the 13,312 there were, sixty seven short of the system being unable to build itself.
;
; Now one block at a time goes through OutBlock, which lives in scratch like every other
; buffer here: a #Reserve is written into the file as zeroes and copied at load, so a
; program that carries its own scratch pays for it twice. What bounds the output is the
; contiguous run the disk can find for it.
;
; OutAt is the cursor, OutBlock which block of the file the window holds, OutHeld whether
; it holds one at all, and OutDirty whether anything has been put in it since it arrived.
OutAt:
0x00 0x00
OutBlock:
0x00
OutHeld:
0x00
OutDirty:
0x00
ImgBlockWant:
0x00
ImgSlot:
0x00
ImgOutBlocks:
0x00
ImgOutTail:
0x00
ImgCursor:
0x00 0x00
ImgAsk:
0x00 0x00
ImgVecMost:
0x00 0x00
NoRoomText:
"no room on the disk for the output
"
MagicSPBT: MagicSPBT:
"SPBT" "SPBT"
+1 -1
View File
@@ -33,7 +33,7 @@ clsToken:
INIB 0x23 ; '#' INIB 0x23 ; '#'
XOR XOR
BNQ clsTryInstruction BNQ clsTryInstruction
INIA 0d0 RSTA
SETD.0 ClsType SETD.0 ClsType
STA.0 STA.0
BRI clsYes BRI clsYes
+9 -4
View File
@@ -297,13 +297,18 @@ LabEnd:
; How many labels there may be, and how many bytes of name between them. ; How many labels there may be, and how many bytes of name between them.
; ;
; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to ; SIZED FOR THE ASSEMBLER ITSELF, which turns out to be the largest thing it is asked to
; build: 555 labels and about 6,800 bytes of name, against CosmOS's 475 and 5,881. Running into ; build: 555 labels and about 6,800 bytes of name, against CosmOS's 650 and 8,081 - CosmOS is
; either limit says so rather than writing past the end of the table. The buffers live ; the bigger of the two now, and was the smaller when this was written. Running into either
; limit says so rather than writing past the end of the table, and that is what it did. The buffers live
; above the program rather than inside it - see the scratch map in Asm.asm. ; above the program rather than inside it - see the scratch map in Asm.asm.
; Fifteen hundred and thirty six names, and sixteen kilobytes to hold them in. Both were
; half that, and the names ran out first: 8,081 bytes of 8,192, which is a hundred and
; eleven - and the thing that ran into it was one ordinary piece of work adding eighteen
; labels. THESE TWO MUST AGREE WITH THE SCRATCH MAP, which says where the room actually is.
LabLimit: LabLimit:
0x03 0x00 0x06 0x00
LabRoom: LabRoom:
0x20 0x00 0x40 0x00
LabNamed: LabNamed:
": " ": "
+12 -12
View File
@@ -28,16 +28,16 @@ numSet:
; The two byte number at DP0 becomes itself plus the one at DP2. ; The two byte number at DP0 becomes itself plus the one at DP2.
numAdd: numAdd:
DPUP.0 0d01 INCD.0
DPUP.2 0d01 INCD.2
LDA.0 LDA.0
LDB.2 LDB.2
CCF CCF
ADD ADD
MVQA MVQA
STA.0 STA.0
DPDN.0 0d01 DECD.0
DPDN.2 0d01 DECD.2
LDA.0 LDA.0
LDB.2 LDB.2
ADD ; Carries in from the low half. Nothing between touches it. ADD ; Carries in from the low half. Nothing between touches it.
@@ -47,16 +47,16 @@ numAdd:
; The two byte number at DP0 becomes itself less the one at DP2. ; The two byte number at DP0 becomes itself less the one at DP2.
numTake: numTake:
DPUP.0 0d01 INCD.0
DPUP.2 0d01 INCD.2
LDA.0 LDA.0
LDB.2 LDB.2
CCF CCF
SUB SUB
MVQA MVQA
STA.0 STA.0
DPDN.0 0d01 DECD.0
DPDN.2 0d01 DECD.2
LDA.0 LDA.0
LDB.2 LDB.2
SUB ; Borrows in from the low half. SUB ; Borrows in from the low half.
@@ -66,14 +66,14 @@ numTake:
; Adds the byte in A to the two byte number at DP0. ; Adds the byte in A to the two byte number at DP0.
numAddByte: numAddByte:
DPUP.0 0d01 INCD.0
LDB.0 LDB.0
CCF CCF
ADD ADD
MVQA MVQA
STA.0 STA.0
BNC numAddByteDone BNC numAddByteDone
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
@@ -82,12 +82,12 @@ numAddByteDone:
; Adds one to the two byte number at DP0. ; Adds one to the two byte number at DP0.
numStep: numStep:
DPUP.0 0d01 INCD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
BNC numStepDone ; It did not wrap, so the high byte is untouched. BNC numStepDone ; It did not wrap, so the high byte is untouched.
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
+2 -2
View File
@@ -14,7 +14,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
SETD.0 Wanted SETD.0 Wanted
@@ -60,7 +60,7 @@ noFile:
#Data #Data
#Base 0x1000 #Base 0x2000
Wanted: Wanted:
#Reserve 0d23 #Reserve 0d23
+59 -13
View File
@@ -12,33 +12,79 @@
; ;
; None of this is initialised data. It is scratch, wanted only while the assembler is ; None of this is initialised data. It is scratch, wanted only while the assembler is
; running, and while it is running everything above its own data is free: the system keeps ; running, and while it is running everything above its own data is free: the system keeps
; below 0x1000, the staging area is only in use during a load, and the Stack comes down ; below 0x1FFF, the staging area is only in use during a load, and the Stack comes down
; from the top. So the addresses are written down here and the file carries none of it. ; from the top. So the addresses are written down here and the file carries none of it.
; ;
; 0x8000 3072 the label index, 768 entries of four ; That sentence said 0x1000 for a while after the system's half of Data Memory was
; 0x8C00 8192 the label names, packed end to end ; doubled, twelve lines above the paragraph that explains the doubling. A stale number is
; 0xAC00 13312 the output file being built ; bad enough; a stale number sitting next to the correction is worse, because whichever
; 0xE000 1792 the vector names, 64 entries of twenty eight ; one a reader takes is a coin toss.
; 0xE700 1758 the reader's stack, six levels of 293
; 0xEE00 368 which files have been included, sixteen names of 23
; ;
; That ends at 0xEF70, with the Stack coming down from 0xFFFF above it - about four ; 0x4000 6144 the label index, 1536 entries of four
; 0x5800 16384 the label names, packed end to end
; 0x9800 256 one block of the output file, on its way to the disk
; 0x9900 18176 free
; 0xE000 1792 the vector names, 64 entries of twenty eight
; 0xE700 2048 the reader's stack, six levels of 301
; 0xEF00 368 which files have been included, sixteen names of 23
;
; IT USED TO START AT 0x8000, and the reason given was that everything above the
; assembler's own data is free. That was true when it was written and stopped being true
; without anything noticing: the system kept below 0x1000 then, and its data now reaches
; 0x1FFF, and the assembler's own moved from 0x1000 to 0x2000 with it. The floor came up
; and the map stayed where it was, leaving sixteen kilobytes between the two that nothing
; touched.
;
; Starting at 0x4000 takes that back. The assembler's data is a little over four kilobytes
; from 0x2000, so there is still nearly four kilobytes of slack in front of this - and room
; for its data to double before the two would meet. `make test` measures that gap now
; rather than trusting this paragraph, and measures the floor above as well, because both
; of those numbers describe the machine AROUND this file and neither is enforced by a line
; of code anywhere.
;
; THE ROOM WENT TO ALL THREE OF THE BUFFERS THAT WERE FULL, and there turned out to be
; three rather than one. The output was the obvious wall - cosmos.bin was 13,245 bytes
; against 13,312, which is sixty seven - so it was given the lot, and the very next thing
; added to the system ran out of LABEL NAMES instead, at 8,081 of 8,192. Two ceilings a
; hundred bytes apart look like one ceiling until the first is lifted.
;
; The index was a hundred and eighteen entries from the same place. So: names doubled,
; index doubled, and the output given what is left, which is still four and a half thousand
; bytes more than CosmOS needs today.
;
; THE OUTPUT IS NO LONGER HELD AT ALL. It used to be built whole in memory and handed over
; at the end, which is what made a buffer of eighteen kilobytes the largest thing this
; machine could assemble. The file is produced in order, so it is written as it is made,
; through one block of window - and the eighteen kilobytes that were its share are free.
;
; What to do with them is not obvious and does not have to be decided today. Nothing here
; is close to full: the names are at half, the index at a third, and the output has no
; ceiling of its own any more. Leaving the room unclaimed is better than sharing it out
; among buffers that do not need it, because an unclaimed page is available to whichever
; one turns out to want it.
;
; That ends at 0xF070, with the Stack coming down from 0xFFFF above it - nearly four
; kilobytes, against the tens of bytes of CALL frames this ever nests. ; kilobytes, against the tens of bytes of CALL frames this ever nests.
; ;
; The reader's levels went from 293 to 301 when an include gained somewhere to be looked
; for: the name in each level is a PATH now, and "/Lib/" is five characters of it. Six
; levels of 301 is 1806, so the room here has to stay above that - which is why the include
; list moved up rather than the stack simply being asked to fit.
;
; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build ; THE TWO THINGS THAT DECIDE THESE SIZES are the largest program it will be asked to build
; and the largest one it will be asked to read. CosmOS is 475 labels and 9,564 bytes of ; and the largest one it will be asked to read. CosmOS is 475 labels and 9,564 bytes of
; output; the assembler itself is 555 labels, about 6,800 bytes of name and 11,648 of output. The ; output; the assembler itself is 555 labels, about 6,800 bytes of name and 11,648 of output. The
; second is bigger than the first, which is worth knowing: the hardest thing this assembles ; second is bigger than the first, which is worth knowing: the hardest thing this assembles
; is not the operating system, it is itself. ; is not the operating system, it is itself.
ScratchLabIndex: ScratchLabIndex:
0x80 0x00 0x40 0x00
ScratchLabArena: ScratchLabArena:
0x8C 0x00 0x58 0x00
ScratchImage: ScratchWindow:
0xAC 0x00 0x98 0x00
ScratchVecNames: ScratchVecNames:
0xE0 0x00 0xE0 0x00
ScratchSrcStack: ScratchSrcStack:
0xE7 0x00 0xE7 0x00
ScratchIncNames: ScratchIncNames:
0xEE 0x00 0xEF 0x00
+55 -4
View File
@@ -189,13 +189,53 @@ srcInclude:
CALL srcRemember CALL srcRemember
CALL srcPush CALL srcPush
; TWO PLACES, TRIED IN ORDER: where you are, and then /Lib. The same rule the shell uses
; for a program it does not recognise, which is where it came from - a name means the
; one beside you if there is one, and the system's otherwise.
;
; It is what a search path is for, and the host assembler has had one since before there
; was a machine to run this on. Without it every source that calls a service has to sit
; in the same directory as services.asm, and the disk cannot be organised at all.
SETD.0 IncWanted SETD.0 IncWanted
SETD.1 SrcName SETD.1 SrcName
CALL srcKeepName CALL srcKeepName
CALL srcRewind CALL srcRewind
BRQ srcIncludeIn
CALL srcInLibrary
CALL srcRewind
BNQ srcIncludeGone BNQ srcIncludeGone
srcIncludeIn:
RET ; Q is zero, out of srcRewind. RET ; Q is zero, out of srcRewind.
; SrcName becomes the same name inside the library directory. Built here rather than kept
; as a second buffer, because what has to survive is the name the file was ASKED for -
; that is what the include-once list holds, and a file found in the library on one line
; and beside you on another is still the same include.
srcInLibrary:
SETD.0 SrcLibrary
SETD.1 SrcName
srcLibraryPrefix:
LDA.0
BRA srcLibraryName
STA.1
INCD.0
INCD.1
BRI srcLibraryPrefix
srcLibraryName:
SETD.0 IncWanted
srcLibraryCopy:
LDA.0
STA.1
BRA srcLibraryDone
INCD.0
INCD.1
BRI srcLibraryCopy
srcLibraryDone:
RET
srcIncludeSkip: srcIncludeSkip:
RSTA RSTA
RSTB RSTB
@@ -483,8 +523,12 @@ srcKeepEnd:
; ---- The current file, as one block so that it can be put aside in one piece ---- ; ---- The current file, as one block so that it can be put aside in one piece ----
; ;
SrcState: SrcState:
; THIRTY TWO RATHER THAN THE TWENTY THREE A NAME NEEDS, because what goes here is a PATH:
; an include not found beside you is looked for in the library, and "/Lib/" plus a name of
; twenty two plus the zero that ends it is twenty eight. Every block of the file is asked
; for by this name, so it has to be the one that resolves, not the one that was typed.
SrcName: SrcName:
#Reserve 0d23 #Reserve 0d32
SrcBlocks: SrcBlocks:
0x00 0x00 0x00 0x00
SrcIndex: SrcIndex:
@@ -504,12 +548,19 @@ SrcEnded:
SrcBuffer: SrcBuffer:
#Reserve 0d256 #Reserve 0d256
; 292 bytes: a name of 23, six numbers of two, one single byte, and the buffer. NOTHING MAY ; 301 bytes: a name of 32, six numbers of two, one single byte, and the buffer. NOTHING MAY
; BE ADDED IN THE MIDDLE OF THE BLOCK ABOVE without changing this to match. ; BE ADDED IN THE MIDDLE OF THE BLOCK ABOVE without changing this to match, and the room
; set aside for six of them in scratch.asm has to be at least six times it.
SrcStateBytes: SrcStateBytes:
0x01 0x24 0x01 0x2D
SrcDepthLimit: SrcDepthLimit:
0d6 0d6
; Where an include is looked for when it is not beside you. One fixed place rather than a
; list somebody sets, for the same reason the shell has one fixed place for programs: a
; list would need somewhere to live between one boot and the next.
SrcLibrary:
"/Lib/"
SrcOne: SrcOne:
0x00 0x01 0x00 0x01
+32 -24
View File
@@ -37,32 +37,35 @@ AsmShapeSelectors:
0d0 0d0 0d0 0d1 0d1 0d1 0d2 0d0 0d0 0d0 0d1 0d1 0d1 0d2
AsmInstructionCount: AsmInstructionCount:
0d64 0d72
AsmInstructions: AsmInstructions:
0x00 0d0 "ADD " 0x10 0d0 "ADD "
0x01 0d0 "SUB " 0x11 0d0 "SUB "
0x02 0d0 "AND " 0x12 0d0 "AND "
0x03 0d0 "OR " 0x13 0d0 "OR "
0x04 0d0 "XOR " 0x14 0d0 "XOR "
0x05 0d0 "NOTA" 0x15 0d0 "NOTA"
0x06 0d0 "NOTB" 0x16 0d0 "NOTB"
0x07 0d0 "SHL " 0x17 0d0 "SHL "
0x08 0d0 "SHR " 0x18 0d0 "SHR "
0x10 0d1 "BRI " 0x60 0d1 "BRI "
0x11 0d1 "BRQ " 0x61 0d1 "BRQ "
0x12 0d1 "BRA " 0x62 0d1 "BRA "
0x13 0d1 "BRB " 0x63 0d1 "BRB "
0x14 0d1 "BRC " 0x64 0d1 "BRC "
0x15 0d3 "BRD " 0x65 0d3 "BRD "
0x1A 0d1 "BNQ " 0x66 0d1 "BNQ "
0x1B 0d1 "BNA " 0x67 0d1 "BNA "
0x1C 0d1 "BNB " 0x68 0d1 "BNB "
0x1D 0d1 "BNC " 0x69 0d1 "BNC "
0x17 0d1 "CALL" 0x70 0d1 "RCAL"
0x18 0d2 "SWI " 0x71 0d1 "CALL"
0x19 0d0 "RETI" 0x72 0d2 "SWI "
0x1F 0d0 "RET " 0x73 0d0 "RETI"
0x74 0d0 "RRET"
0x75 0d0 "RET "
0x76 0d0 "SRET"
0x20 0d0 "RSTA" 0x20 0d0 "RSTA"
0x21 0d0 "RSTB" 0x21 0d0 "RSTB"
0x22 0d0 "INCA" 0x22 0d0 "INCA"
@@ -97,10 +100,15 @@ AsmInstructions:
0x4B 0d6 "STD " 0x4B 0d6 "STD "
0x4C 0d3 "MVSD" 0x4C 0d3 "MVSD"
0x4D 0d3 "MVDS" 0x4D 0d3 "MVDS"
0x4E 0d3 "DPUA"
0x4F 0d3 "DPDA"
0x50 0d3 "DPUW"
0x51 0d3 "DPDW"
0xD0 0d2 "OUTQ" 0xD0 0d2 "OUTQ"
0xD1 0d2 "OUTA" 0xD1 0d2 "OUTA"
0xD2 0d2 "OUTB" 0xD2 0d2 "OUTB"
0xE0 0d2 "INA " 0xE0 0d2 "INA "
0xE1 0d2 "INB " 0xE1 0d2 "INB "
0xF0 0d0 "NOP " 0xF0 0d0 "NOP "
0xFE 0d0 "WAIT"
0xFF 0d0 "HALT" 0xFF 0d0 "HALT"
+2 -2
View File
@@ -15,7 +15,7 @@
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
SETD.0 Wanted SETD.0 Wanted
@@ -128,7 +128,7 @@ noFile:
#Data #Data
#Base 0x1000 #Base 0x2000
Wanted: Wanted:
#Reserve 0d23 #Reserve 0d23
-1
View File
@@ -189,7 +189,6 @@ vecReadFields:
STA.1 STA.1
INCD.0 INCD.0
LDA.0 LDA.0
SETD.1 VecHandler
INCD.1 INCD.1
STA.1 STA.1
INCD.0 INCD.0
+492 -32
View File
@@ -17,9 +17,19 @@ for itself.
- Interactive Shell: Read commands from the SplitBit console and continue until `exit` or - Interactive Shell: Read commands from the SplitBit console and continue until `exit` or
the end of input. the end of input.
- SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk. - SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk.
- Paths: Anywhere a filename is taken, a path may be given instead - names with `/`
between them, with `.` and `..`. Directories are read but not yet made; the host tool
makes them.
- Working Directory: `cd` moves the machine, `dir` lists where it is, and the prompt says
where that is once it is not the root. A program may move too, and the shell puts the
working directory back when the program stops.
- Making Directories: `mkdir` and `rmdir` on the machine, and files written where their
path says, so a disk can be organised without the host tool.
- Loadable Applications: Validate SBEX files, copy their Program and Data segments into - Loadable Applications: Validate SBEX files, copy their Program and Data segments into
the addresses for which they were assembled, and start them at their declared entry the addresses for which they were assembled, and start them at their declared entry
point. point.
- Invocation By Name: A word the shell has no command for is looked for on the disk as
`<name>.sbx`, and loaded and started if it is there. Built-in commands are tried first.
- Resident Services: Applications can print strings and numbers, read lines, receive their - Resident Services: Applications can print strings and numbers, read lines, receive their
command arguments, read and write files, and return to the shell through named software command arguments, read and write files, and return to the shell through named software
interrupts. interrupts.
@@ -81,8 +91,12 @@ CosmOS currently provides these built-in commands:
| Command | Description | | Command | Description |
| -- | -- | | -- | -- |
| `dir` | List the files on the mounted disk and their sizes. | | `dir` | List the files on the mounted disk and their sizes. |
| `load <file>` | Read and validate an SBEX application, then place its code and data where its header requests. | | `load <path>` | Read and validate an SBEX application, then place its code and data where its header requests. |
| `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. | | `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. |
| `cd [path]` | Go to a directory, or to the root with nothing after it. |
| `mkdir <path>` | Make a directory. |
| `rmdir <path>` | Remove one, if it is empty. |
| `<name> [words]` | Any word the shell does not recognise is looked for on the disk as `<name>.sbx`, and loaded and started if it is there. |
| `delete <file>` | Remove a file from the filesystem and release its blocks. | | `delete <file>` | Remove a file from the filesystem and release its blocks. |
| `rename <file> <to>` | Give a file a different name without moving its contents. | | `rename <file> <to>` | Give a file a different name without moving its contents. |
| `monitor` | Enter monitor mode, in which the prompt becomes `*` and the commands below are also available. | | `monitor` | Enter monitor mode, in which the prompt becomes `*` and the commands below are also available. |
@@ -115,14 +129,227 @@ For example:
> run > run
``` ```
Loading and running are separate operations for now. A loaded program may be run again Or, equivalently:
without being read from disk again, which is useful both as a monitor facility and as a
test that CosmOS correctly restores its Stack and vector table after every run. ```text
> Snake
```
Loading and running remain separate operations, and both of them remain. A loaded program
may be run again without being read from disk again, which is useful both as a monitor
facility and as a test that CosmOS correctly restores its Stack and vector table after
every run; and `load` is how the monitor puts an arbitrary file in front of itself, which
is a thing typing a name deliberately cannot do.
### Paths:
Everywhere CosmOS takes a filename it will take a path: names with `/` between them,
walked from the root, with `.` meaning where you are and `..` meaning the directory above.
`..` from the root is the root. A bare name is a path of one name, so nothing written
before directories existed had to change.
```text
> load /Apps/Snake.sbx
> Type /Notes/today.txt
```
**Programs did not have to be taught any of this.** Path resolution lives inside
`sbfsFind`, below the services, so `osFileInfo`, `osFileBlock`, `osFileSave`,
`osFileDelete` and `osFileRename` all still take a pointer to a name - and a path is
simply a longer name. `Type`, `More`, `Edit` and the assembler gained subdirectories
without a line being changed in any of them.
Each *name* along a path is still the 22 characters a directory entry holds, and a longer
one is refused rather than cut short, because a name cut to 22 characters is a different
name that might well be some other file's.
### The Working Directory:
`cd` moves the machine. A path beginning with `/` is measured from the root and anything
else from where you are, so a bare name means a file in the current directory - which is
the whole of what a working directory is, and no program had to be told.
```text
> cd /Apps
/Apps> dir
/Apps> cd Deep
/Apps/Deep> cd ..
/Apps> cd
>
```
`cd` with nothing after it goes to the root, which is the only place always there.
**The prompt says where you are, but only when that is not the root**, so a machine nobody
has moved about on looks exactly as it always did. Nothing stores the path: the working
directory is an entry index and two bytes, and the text on the prompt is worked out again
each time by walking the chain of parents upward.
That walk goes from where you are up to the root, so the names arrive deepest first and
are written into the buffer **backwards, from its end**. When they do not all fit, what is
already down is the deep end of the path, which is the end worth keeping - so the prompt
is cut at the front and says so:
```
...opqrst03/abcdefghijklmnopqrst04/.../abcdefghijklmnopqrst08>
```
**Three limits, and the smallest is not the one you would guess.** A path handed to any
one operation is capped at 95 characters up to the last separator plus a name of 22; the
host tool carries 512, which only means it can build a tree CosmOS cannot name in one
piece. Neither of those binds anything: the longest path on a full install is 21
characters. What binds is the prompt's 127 bytes - and **nothing caps how deep the
directories go**, because `mkdir a` and `cd a` are each well inside every limit and can be
typed all day.
Before the walk was bounded it wrote past the front of that buffer and into whatever the
assembler had put below it, which was the shell's own command names. Six directories of 22
characters was enough. The first five bytes to go were the word `exit`, so the shell
stopped recognising the command for leaving - a fault with no plausible connection to the
directory you happened to be standing in.
`dir` lists the directory you are in rather than the whole disk.
A program can move too, with `osChangeDir`, and **the shell puts the working directory
back when the program stops** - the same discipline it already applies to the Stack and to
the vector table, and for the same reason. A program is entitled to move about; the shell
is entitled to find itself where it left off.
Whenever what a relative path means changes - a `cd`, a program calling `osChangeDir`, a
program exiting - CosmOS forgets the file it was remembering. That cache is keyed on the
path as it was typed, so `notes.txt` is the same key in two directories and nothing about
the entry it holds would look wrong. It is the kind of stale that gets believed rather
than noticed.
### Making And Removing Directories:
`mkdir` and `rmdir` are the machine's own, so a disk can be organised without the host
tool. A file a program writes goes where its path says, and a bare name means the
directory you are in.
**A directory costs one entry and no blocks at all.** Its start, block count and tail are
all zero, which is what keeps the flat array of entries the whole allocation map - with
files laid down contiguously, every block is inside some entry's range or it is not, and
an entry with no range is in nobody's way.
Making the first directory on a disk is what raises it from version one to version two,
because it is the only thing that makes the difference between them real. A disk stays
readable by anything that has never heard of a directory right up until it actually has
one.
**A disk may have at most 8,191 directory blocks**, which is 65,528 entries, and that
number comes from the parent field rather than from anything about size. A parent is an
index *plus one* in two bytes, so entry 65,535 has no parent number at all: adding one
wraps to zero, and zero means the root.
The failure is worth describing, because it is the shape of failure this format has to
watch for. Such an entry does not refuse what is put inside it. It writes a parent of zero
and the thing lands in **the root**, while whatever asked is told it went where it asked
for. Looking in that directory afterwards finds nothing, because the search is for a
parent the entry does not carry - so the same create succeeds again, and again, filling
the root with entries of one name. Two entries of one name in one directory is precisely
what `rename` refuses on the grounds that a search answers with whichever it meets first
and the rest can never be reached again; this made them by the handful, one per attempt.
Both implementations refuse to format past the bound, and refuse to read a disk that
claims it - because a disk claiming it was made by something that never checked.
Four things are refused, and each refusal is the reason a separate command exists:
**`rmdir` will not take a file and `delete` will not take a directory.** Neither can be
the one that removed more than was asked for.
**A directory with anything in it is refused.** This is not politeness. A parent is an
entry *index*, and a freed index is handed to the next thing created - so the children of
a directory removed from under them would turn up inside whatever took its place. Nothing
points downward, so there would be no way to find them afterwards and no way to notice.
**A name already used in that directory is refused.** Two entries with one name in one
place is a directory that cannot be searched sensibly: a search answers with whichever it
meets first, and the other becomes unreachable without ever having been deleted. The same
name in a *different* directory is fine, and is the point of the exercise.
**`rename` will not move anything.** Only the twenty two bytes of the name change and the
parent is not among them, so `rename a/x b/y` would be a lie the disk went along with.
`Tests/agree.sh` builds the same disk twice, once with SplitDisk and once with CosmOS, and
compares the images byte for byte. 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.
### Starting An Application By Name:
A word the shell has no command for is not immediately an error. Before saying so, the
shell adds `.sbx` to it unless it is already there, looks for a file of that name, and if
one is there loads it and starts it exactly as `load` and `run` would. Whatever followed
the word reaches the program through `osArgument`, the same way and by the same route as
whatever follows `run`.
Three properties of this are deliberate:
**Built-in commands are tried first and always win.** The search happens only after the
whole dispatch chain has failed to match, so a file named `dir.sbx` cannot become `dir`.
The commands that are worth trusting when the disk is the thing being doubted stay
trustworthy.
**The extension is what makes a file reachable by name.** Typing `notes` looks for
`notes.sbx`, and typing `notes.txt` looks for `notes.txt.sbx`. A text file therefore
cannot be started by typing what it is called, whatever happens to be inside it. Only
`load` reaches a file by its literal name.
A path works here too, so `/Apps/Say hello` starts `/Apps/Say.sbx` and gives it `hello`.
**Two places are tried, in order: where you are, and then `/Apps`.** The first is what makes
a program you are working on the one that runs; the second is what lets `Snake` work from
anywhere without a copy of it in every directory. A word that already begins with `/` has
said where to look, so only that place is tried. Neither is stored anywhere, so there is
nothing to configure and nothing to go stale - a search path somebody could set would need
somewhere to live between one boot and the next, and there is no such place yet.
**A file that is found but is broken says so.** If `notes.sbx` exists and is not an SBEX
program, typing `notes` reports `not a program` rather than `I do not know: notes`.
Reporting an unknown command about a file that is sitting on the disk would send somebody
looking in the wrong place.
Names are matched exactly, including case, because every other name on the filesystem is.
What limits the typed word is the buffer it is built in rather than the format: each name
along a path is still twenty two characters, and the path walker refuses a longer one
rather than cutting it down. A word that will not fit is reported as unknown, which is the
truth, since nothing the shell can reach is called that.
CosmOS also boots without a disk. It reports that no filesystem was found, leaves the CosmOS also boots without a disk. It reports that no filesystem was found, leaves the
shell and memory monitor available, and refuses commands that require a mounted disk shell and memory monitor available, and refuses commands that require a mounted disk
without stopping the machine. without stopping the machine.
## What Is On The Disk:
`make -C Programs cosmos-disk` builds the disk this system is meant to be met on, and it is
laid out in three directories:
| Where | What |
| -- | -- |
| `/Apps` | The programs. The second place the shell looks for a word it does not recognise, so anything here starts by name from anywhere on the disk. |
| `/Source` | The things you name to the assembler: CosmOS itself, the assembler itself, and small programs to read. |
| `/Lib` | The things those include. Everything here is named by an `#Include` somewhere and by nothing else, which is what makes it a library rather than a source. |
The split is by role rather than by which directory the host keeps a file in, and it only
works because **an include is looked for where you are and then in `/Lib`** - the same rule
the shell uses for programs, applied to the assembler. Without that search every source
that calls a service would have to sit beside `services.asm`, and there would be nothing to
organise.
So the machine rebuilds itself from its own disk:
```text
> cd /Source
/Source> Asm cosmos.asm
wrote cosmos.bin: program 9778, data 3346, labels 648
/Source> Asm Asm.asm
wrote Asm.sbx: program 7570, data 4114, labels 562
```
Both come out byte for byte what the host assembler makes from the same source.
## Included Applications: ## Included Applications:
`Programs/CosmOS/Apps` holds what the shell can load, and the application disk is built `Programs/CosmOS/Apps` holds what the shell can load, and the application disk is built
@@ -134,10 +361,20 @@ from every assembly file in it. Several are old programs written for the bare ma
| Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. | | Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. |
| Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. | | Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. |
| Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. | | Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. |
| Files | Writes a file, reads it back, renames it and deletes it, in 645 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. | | Reboot | Starts the machine again, in 45 bytes. Writes a port rather than asking the system, because a reset has to work when the system does not. |
| Once | Asks the loader to start something else on the next start, and only that one, in 569 bytes. |
| Status | Says what the last program made of what it was asked to do, in 222 bytes. The shell keeps the number and does not print it; this is how a person looks. |
| Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. |
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. | | Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
| Edit | A line editor. | | Edit | A line editor. |
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. | | Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
| Pour | Writes a file a block at a time, never holding more than one block of it. Each block is filled with a byte naming itself, so a block written to the wrong place shows up as content rather than as a length. |
| Copy | Copies one path to another a block at a time, including an empty file or one larger than Data Memory. |
| Compare | Compares two files a block at a time, stopping at their real tails rather than comparing unused bytes in the final disk blocks. |
| Wander | Goes to the directory it is given and reads a file there by a bare name. The only thing that moves the machine from inside a program, and so the only thing that can check the shell puts the working directory back afterwards. |
| More | A forward-only pager. Space advances a screen, Return one line, and q stops. |
### The Monitor: ### The Monitor:
@@ -229,7 +466,20 @@ Typed in as bytes, checked by disassembling it back, and run. It ends with `SWI
`Edit` is the first program on this machine that makes a file a person typed - every byte on every disk before it was put there by the host tool. It is line oriented in the manner of `ed`: `l` lists, `a` adds at the end, `i` and `c` and `d` take a line number, `w` writes and `q` stops. `Edit` is the first program on this machine that makes a file a person typed - every byte on every disk before it was put there by the host tool. It is line oriented in the manner of `ed`: `l` lists, `a` adds at the end, `i` and `c` and `d` take a line number, `w` writes and `q` stops.
It includes nothing but `services.asm` and `text.asm`: the filesystem and the console are the system's, asked for rather than carried. That is what took it from 4,941 bytes to 1,983 without a line of its own logic changing - and the way that was checked is worth knowing, because the recorded output of the `cosmosEdit` test did not move by a single byte across the rewrite. It includes nothing but `services.asm` and `text.asm`: the filesystem and the console are the system's, asked for rather than carried. That is what brought `Edit` down from 4,941 bytes to 2,157 bytes without a line of its own logic changing - and the way that was checked is worth knowing, because the recorded output of the `cosmosEdit` test did not move by a single byte across the rewrite.
**A line it reads in is at most 128 characters**, the same length a line is everywhere else
on this machine, and a file with a longer one is refused rather than opened. Refused rather
than shortened, because this is an editor: a line cut on the way in would be written back
cut, and the file damaged by having been looked at.
That limit was not there at all until a source file found it. The buffer is followed in
memory by the head of the document and the pointer the line allocator hands out, so a 94
character line wrote characters over both - a 31 line file opened as 3, and opening it a
second time walked a list that led back into itself for ever, with the emulator still
running and the machine never answering again. Typing a long line was always safe, because
`osReadLine` is told how much room there is; only the file being read went unchecked, which
is why a new document behaved and a source file did not.
It keeps the document as a **linked list of lines** rather than one buffer with newlines in it. Each line says where the next one is, how long it is, and then its bytes. Inserting is two pointers changed and nothing moved; with a flat buffer it would mean shifting every byte after the edit, on a machine whose only block move is a device asked politely. The price is that deleted lines are not reused, so a heavy session uses more room than the document needs and writing it out is what tidies up. It keeps the document as a **linked list of lines** rather than one buffer with newlines in it. Each line says where the next one is, how long it is, and then its bytes. Inserting is two pointers changed and nothing moved; with a flat buffer it would mean shifting every byte after the edit, on a machine whose only block move is a device asked politely. The price is that deleted lines are not reused, so a heavy session uses more room than the document needs and writing it out is what tidies up.
@@ -239,14 +489,53 @@ These are ordinary SBEX files on SBFS. None of them is built into the operating
a disk can be filled from either side: the host tool puts files on, and so does the machine, a disk can be filled from either side: the host tool puts files on, and so does the machine,
which assembles its own now. which assembles its own now.
### A Clean Install:
`make cosmos-disk` in `Programs/` lays down a disk the machine can start itself from, and
`make run-cosmos` starts it - with no boot image named, so the emulator shadows its ROM and
reads the disk for everything else.
```
/ Apps Source Lib System
/Apps what you run, and the second place the shell looks for a word
/Source what you assemble, including stage1.asm and stage2.asm
/Lib what those include
/System/Boot cosmos.bin, and the slots the loader lives in
```
**The loader's own source is on the disk**, which means the machine can rebuild what starts
it: `Asm stage2.asm` produces the bytes that go in a boot slot, and everything stage two
includes is already in `/Lib`. Stage one is the exception and always will be - it is the
ROM, and the one part of this a disk cannot replace.
No `boot.cfg` is written. Stage two falls back to `/System/Boot/cosmos.bin` when there is
none, and a clean install having nothing to configure is the right default.
`make run-cosmos-direct` hands the system over the old way instead, memory placed from
outside with nothing on the disk consulted. That is what a debugger does, and it is what to
use when the thing being debugged is the boot chain, since it skips the boot chain.
## The Application Model: ## The Application Model:
CosmOS divides the two SplitBit address spaces by convention: CosmOS divides the two SplitBit address spaces by convention:
| Memory | CosmOS | Loaded application | | Memory | CosmOS | Loaded application |
| -- | -- | -- | | -- | -- | -- |
| Program Memory | `0x0000` through `0x1FFF` | `0x2000` and above | | Program Memory | `0x0000` through `0x3FFF` | `0x4000` and above |
| Data Memory | `0x0000` through `0x0FFF` | `0x1000` and above | | Data Memory | `0x0000` through `0x1FFF` | `0x2000` and above |
Both of CosmOS's halves were doubled once it outgrew the first ones. **The division is a
convention and nothing enforced it**, so CosmOS quietly grew past `0x1FFF` and the next
program loaded landed on top of its own code - which does not fail where it happens, it
fails later, in whatever part of the shell the program happened to cover. `make test` now
measures both segments against the numbers in this table, so the table is checked rather
than merely written down.
**The table is checked against itself as well.** The first version of that check read only
the CosmOS column, and so it passed a table whose Data row gave the system `0x3FFF` and an
application `0x2000` - two columns that cannot both be true, sitting next to each other.
Measuring one number against the code and never against the number beside it is how a
specification contradicts itself in public.
Applications state their actual Program and Data addresses with `#Base`. The SplitBit Applications state their actual Program and Data addresses with `#Base`. The SplitBit
assembler then writes an SBEX loadable image containing those addresses, the entry point, assembler then writes an SBEX loadable image containing those addresses, the entry point,
@@ -275,7 +564,7 @@ A minimal CosmOS application therefore looks like this:
#Include services.asm #Include services.asm
#Program #Program
#Base 0x2000 #Base 0x4000
start: start:
SETD.0 Message SETD.0 Message
@@ -283,7 +572,7 @@ start:
SWI osExit SWI osExit
#Data #Data
#Base 0x1000 #Base 0x2000
Message: Message:
"Hello from CosmOS." "Hello from CosmOS."
@@ -311,8 +600,16 @@ Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, w
| osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. | | osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. |
| osFileInfo | DP0 names a file. Q is zero if it is there, and DP3 comes back holding how many blocks it occupies. | | osFileInfo | DP0 names a file. Q is zero if it is there, and DP3 comes back holding how many blocks it occupies. |
| osFileBlock | DP0 names a file, DP1 says where to put a block of it, A and B together are which block counting from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes belong to the file. | | osFileBlock | DP0 names a file, DP1 says where to put a block of it, A and B together are which block counting from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes belong to the file. |
| osChangeDir | DP0 names a directory. Q is zero if the machine is now in it. What a program changes here, the shell puts back when the program stops. |
| osFileStart | DP0 names a file, DP3 is how many whole blocks and A is the bytes left over in the last one. Q is zero if a write is now open. Nothing already on the disk is touched. |
| osFileWrite | DP1 is a block, A and B together are which block of the file it is, counting from zero. Q is zero if it was written. An index past the end of the file is refused. |
| osFileDone | DP3 is how many whole blocks it came to and A the bytes left over. The old file goes and what was written takes its name, at that size. Q is zero if it was committed. |
| osFileFetch | DP1 is where a block should go, A and B together are which block. Reads back a block of the file being written. |
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. | | osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
| osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. | | osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. |
| osLastStatus | Q answers what the last program exited with: 0 it did what it was asked, 1 it did not, 2 it was asked wrongly. A program may give its own meanings if it says so. |
| osBootState | Q answers how the last start went: 0 settled, 1 trying, 2 fell back. A machine with no disk answers settled, because there is nothing there to be unsettled about. |
| osBootSettle | Puts it back to settled, which is how a machine that fell back is told the situation has changed. Q is zero if the disk took it. **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 cannot check. |
``` ```
#Include services.asm #Include services.asm
@@ -395,33 +692,12 @@ Running off the end is how a reader finds out it has finished, so it gets an ans
`Programs/CosmOS/Apps/Stream.asm` reads an 84,000 byte file through a 256 byte buffer, then reads a small file both ways - whole with `osFileRead` and streamed - and checks that the two agree. `Programs/CosmOS/Apps/Stream.asm` reads an 84,000 byte file through a 256 byte buffer, then reads a small file both ways - whole with `osFileRead` and streamed - and checks that the two agree.
`Programs/CosmOS/Apps/Files.asm` does the whole round trip - write, read, report, rename, delete - in 645 bytes, and includes nothing but the service names. `Programs/CosmOS/Apps/Files.asm` does the whole round trip - write, read, report, rename, delete - in 675 bytes, and includes nothing but the service names.
`osArgument` is how a program is told what it is for. Everything written before it did the same thing however it was started, which is fine for a program that greets you and no use to one that edits a named document. What arrives is the whole rest of the line, spaces and all, rather than a list of words: what counts as an argument is the program's business, and handing over what was typed is the system's. `osArgument` is how a program is told what it is for. Everything written before it did the same thing however it was started, which is fine for a program that greets you and no use to one that edits a named document. What arrives is the whole rest of the line, spaces and all, rather than a list of words: what counts as an argument is the program's business, and handing over what was typed is the system's.
A handler is entered with the caller's registers exactly as they were, because an interrupt frame is pushed rather than cleared. That is why a service can be given a pointer in DP0 and a count in B without any of it being copied anywhere first. A handler is entered with the caller's registers exactly as they were, because an interrupt frame is pushed rather than cleared. That is why a service can be given a pointer in DP0 and a count in B without any of it being copied anywhere first.
### How A Service Answers:
The same thing that makes an interrupt safe makes a service mute. RETI restores every register from the frame, so whatever a handler worked out is thrown away on the way out - which is exactly right for a device interrupting at a moment nobody chose, and useless for a service that was asked a question.
A service answers by **writing into its own frame**, over the saved register, and letting RETI put it back. MVSD copies the Stack Pointer into a Data Pointer and the frame sits just above it, so returning a byte in Q is three instructions:
```
answer:
INIA 0d42
MVSD.1
DPUP.1 0d02 ; The saved Q. See the frame table under Interrupts.
STA.1
RETI
```
**Which registers a service may answer in is the convention CALL already has: Q and DP3.** A subroutine cannot hand back A, B or Data Pointers 0 to 2 because RET puts them back; a service *could* write over any of them and should not, for exactly the reason that list exists. A caller is entitled to find what it kept still there.
**Only the handler itself can do this.** The offsets are from wherever the Stack Pointer is, and a CALL moves it by ten - so a routine called by a handler that tried the same thing would be writing into its own return address. The poke belongs inline, next to the RETI.
A service that has nothing to say does nothing, and the caller's registers arrive back untouched. That is worth knowing from the other side too: a service cannot corrupt a register by accident, only by deciding to.
## The Filesystem Library: ## The Filesystem Library:
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one. The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one.
@@ -443,6 +719,115 @@ Finding a file and listing what is there are different jobs. sbfsFind searches f
A file's size is settled when it is made, because nothing can grow one afterwards. Files are laid down contiguously, so the block after a file usually belongs to somebody else. A program that does not know how much it will write has to guess high and accept the slack, or build its output elsewhere and make the file once the size is known. A file's size is settled when it is made, because nothing can grow one afterwards. Files are laid down contiguously, so the block after a file usually belongs to somebody else. A program that does not know how much it will write has to guess high and accept the slack, or build its output elsewhere and make the file once the size is known.
### Writing A File Too Big To Hold:
`osFileSave` is handed a whole document at once, which is what a text editor has. A program
that produces its output a piece at a time - an assembler, say - would have to hold all of
it first, and the largest thing on this machine would then be limited by memory rather than
by the disk.
So there is the other half of the streaming pair. `osFileInfo` and `osFileBlock` read a
file a block at a time; `osFileStart`, `osFileWrite` and `osFileDone` write one.
```text
SETD.0 Name
SETD.3 0x00 0x06 ; six whole blocks
INIA 0d40 ; and forty bytes after them
SWI osFileStart
...for each block: DP1 the bytes, A and B which block...
SWI osFileWrite
SETD.3 0x00 0x06 ; and what it came to, which need not be
INIA 0d40 ; what was asked for
SWI osFileDone
```
**One write is open at a time, and the system holds it rather than the program.** 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`.** The room for the whole file
is taken at the start, so a disk that cannot hold it says so while the old one is still
there. That is stronger than `osFileSave` can manage, where the size is only known once the
caller already has every byte in hand.
**The size asked for need not be the size it comes to.** Some sizes are not knowable until
the last byte is out - the assembler cannot say how many vectors a program installs until
it has resolved them, and by then the file it is writing into has to exist. So the room is
taken generously at the start, where running out costs nothing, and `osFileDone` is told
the truth. The blocks that were asked for and not used go back.
`osFileFetch` reads a block of the file back, which is what lets a program keep only one
block of it in hand. Anything producing two parts of a file at once - source that says
`#Program` and `#Data` in whatever order it likes - has to be able to put a block down, go
and write somewhere else, and pick it up again where it left off.
Two limits differ between the two. `osFileSave` is handed a byte count in two registers and
so cannot write more than 65,535 bytes; `osFileStart` is told blocks and a tail, the way an
entry holds a size, and reaches the whole disk. And `osFileWrite` refuses an index past the
end of the file - 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.
### Reading Ahead:
A file is read front to back, so when a program asks for a block, the one after it is
almost certainly wanted next. `sbfsReadOne` asks the disk for it straight away and hands
the caller the block it wanted - so the transfer happens while the program is busy with
what it already has, and the wait is mostly gone by the time it comes back.
Nothing is done differently and nothing is done out of order. The machine simply stops
standing still.
**It is not done for directory searches, and that is not an oversight.** A scan stops the
moment it matches, so the next block is one nobody will ever look at: it costs a transfer to
fetch and another wait to throw away. Tried there, it was nineteen per cent *slower*. Read
ahead is a bet that the next block is wanted, and a search is exactly the case that hopes it
is not.
What it is worth, printing a fourteen kilobyte file:
| Cycles a block | Without | With |
| -- | -- | -- |
| 0 | 936,626 | 962,959 |
| 2,000 | 1,064,498 | 976,882 |
| 10,000 | 1,576,562 | 1,032,889 |
The second column barely moves. From an instant disk to a slow one the cost rises seven per
cent, where without it the same change costs sixty eight - which is the point: **a machine
that reads ahead stops caring very much how fast its disk is.** The three per cent it costs
at zero is the bookkeeping, paid when there is nothing to hide behind it.
### And Waiting For What Is Left:
Read ahead hides most of the wait and cannot hide all of it. What remained was a loop
asking the disk's status port over and over, which is work the machine is doing and memory
it is touching to find out that nothing has happened yet.
`sbfsWaitDisk` uses `WAIT` now. It tests the status port, and only if the disk is still
busy does it stop - the CPU is put down until a device raises a line, and the disk raises
one when it finishes. **Asking first is what makes it safe:** if the disk finished in the
gap between the test and the `WAIT`, its line is already standing and the `WAIT` does
nothing rather than sleeping through the answer.
There is no handler and no vector. The shell keeps the Interrupt Flag down, and a `WAIT`
wakes on a line whether or not anybody intends to answer it, taking it down on the way
past. Printing the same fourteen kilobyte file:
| Cycles a block | Total | Of that, the 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 middle column stops moving.** What the program costs in memory is now the same
whatever the disk does, and the difference is time spent with the bus quiet. On this
emulator that changes nothing anybody can see; on hardware it is the difference between a
CPU contending for memory with everything else and a CPU standing out of the way.
### Saving Something Twice: ### Saving Something Twice:
Which is why saving a document is not the same as writing a file, and why sbfsSaveFile exists rather than each tool doing it. A file that has grown will usually not fit where it was, so saving it means putting it somewhere else and letting go of where it was - and **the obvious order is a trap**: Which is why saving a document is not the same as writing a file, and why sbfsSaveFile exists rather than each tool doing it. A file that has grown will usually not fit where it was, so saving it means putting it somewhere else and letting go of where it was - and **the obvious order is a trap**:
@@ -464,12 +849,87 @@ A create can be refused for want of a run long enough even on a disk with plenty
**That is what renaming is for.** It looks like a convenience and it is the safety mechanism: it is the only one of the three operations that moves no data - a name lives in the directory entry, so renaming writes twenty two bytes into one block - which makes it the only one that can be left until last and relied on not to fail. **That is what renaming is for.** It looks like a convenience and it is the safety mechanism: it is the only one of the three operations that moves no data - a name lives in the directory entry, so renaming writes twenty two bytes into one block - which makes it the only one that can be left until last and relied on not to fail.
#### Exactly what that promises:
The ordering protects the original against **every way a save can fail while it is running**, and it is worth naming those, because they are the ones that actually happen: there is no run of free blocks long enough, or none at all; the disk refuses a block write; the name turns out to belong to a directory; the writer gives up part way through. In all of them the file that was already there is untouched, and what is lost is the temporary, which nothing had come to depend on yet.
It is **not** power-loss atomic, and nothing about SBFS claims it is. The commit is two block writes - delete the old entry, then give the temporary its name - and a machine that stops between them leaves the old file gone and the new one under the temporary's name. Both writes are to the directory, so `sbfs.part` or `sbfs.out` is sitting there holding every byte of the work; the data survives and the name does not, and putting it right is one `rename` typed by hand. `dir` marks it `<unfinished>` so that it can be found, which is the whole of the recovery this format offers.
Closing that window means a journal or a second copy of the directory, and both are a great deal of machinery to buy back a two-write gap on a machine with no power failures to speak of. The honest description is the one to write down: **safe against the failures of ordinary operation, not against the machine stopping.**
#### What tells a temporary from a file:
While the save runs the temporary is an ordinary entry in every way that matters - it holds real blocks and answers to a name - and the only thing that makes it different is that nobody has committed it yet. That is **not a property of its contents.** The same bytes become the finished file the instant the rename lands, so there is nothing to put inside it that would be true. It belongs in the entry, which is the thing the commit changes, and it is **flag bit `0x04`**.
It used to be told apart by being *called* `sbfs.part` or `sbfs.out`, and those are names anybody is entitled to give a file of their own. Starting a save deleted whatever answered to one, as stale scratch - so saving anything at all in a directory destroyed your file of that name there, silently, and the first you would know of it is going to look for it. A file that does not carry the flag now belongs to somebody, and the save is **refused** rather than helping itself to the name.
The same bit is what makes an interrupted save recoverable. Both listings show an unfinished write rather than sizing it, because the size in the entry is the room it asked for and not what was written into it:
```
> dir
stranded.txt <unfinished>
```
Rename it to keep the data, delete it to give the blocks back. Nothing reclaims it on its own; a boot-time consistency check could, and this is the field it would read.
**A committed file never carries the bit**, so a disk this writes is byte for byte the disk the older code wrote - the agreement tests compare whole images and say so. Only the wreckage of a save that stopped looks different, and code that has never heard of the flag reads that as an ordinary file, which is exactly what it did before.
Finding room is a walk through the directory rather than a lookup, because there is no allocation table. With files laid down contiguously the directory already says which blocks are spoken for, and a second copy of that would be a second thing to keep right. The free count in the superblock is kept up to date but it is a note rather than the truth: it can be worked out again from the directory, and the directory is the one to believe. Finding room is a walk through the directory rather than a lookup, because there is no allocation table. With files laid down contiguously the directory already says which blocks are spoken for, and a second copy of that would be a second thing to keep right. The free count in the superblock is kept up to date but it is a note rather than the truth: it can be worked out again from the directory, and the directory is the one to believe.
A file's length is its block count times 256 plus its tail, which is the same as putting the block count in the high byte and the tail in the low one. Nothing pads a file out, so the bytes after the end of one are whatever else happened to be in that block, and it is the reading program's business to stop where the tail says. A file's length is its block count times 256 plus its tail, which is the same as putting the block count in the high byte and the tail in the low one. Nothing pads a file out, so the bytes after the end of one are whatever else happened to be in that block, and it is the reading program's business to stop where the tail says.
The other implementation of this format is SplitDisk, on the host. Nothing is shared between the two but the specification, so a change to either has to be a change to both. The other implementation of this format is SplitDisk, on the host. Nothing is shared between the two but the specification, so a change to either has to be a change to both.
### What A Program Made Of It:
`SWI osExit` takes a status **in A**: zero if the program did what it was asked, one if it
did not, two if it was asked wrongly. A program may give its own meanings if it says so, and
`Compare` does - one there means the files differ, which is a result rather than a failure.
**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 setting it to a small number costs four instructions where A costs one.
**The shell keeps the number and does not print it.** A program that failed has already said
so in words, and a number beside that would be noise. `osLastStatus` hands it back and
`Status` is the program that shows it. The indirection is the point: this number is for the
thing that cannot read words - whatever comes to run programs in sequence and has to decide
whether to run the next one.
Marking all fifty eight exits found a defect on its 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.
### How A Service Answers:
A handler arrives with the caller's registers pushed rather than cleared, and **`RETI`
restores every one of them** - which is what makes an interrupt safe to arrive at an
arbitrary moment, since the interrupted code cannot tell it happened. A service is not
arbitrary. It was asked for, and it has something to say.
It says it with `SRET`, which is `RET` adapted to an interrupt frame: **A, B and Data
Pointers 0 through 2 come back, the saved Q and Data Pointer 3 are dropped, and the
Interrupt Flag is put back from the frame.** So a service answers in exactly the registers
a subroutine answers in, and there is one rule on this machine rather than two.
Before it existed, a handler with an answer wrote into its own frame:
```asm
MVSD.2
DPUP.2 0d02 ; the saved Q, by an offset it had to know
STA.2
RETI
```
Thirty places did that, each knowing the frame's layout by heart, and all thirty would have
gone quietly wrong the day the frame gained a field. None of them knows it now.
`RETI` is still right for a **hardware** handler, which has nothing to say and must leave
no trace. The two returns are not a choice of style: one says *I was never here* and the
other says *here is your answer*.
### What A Subroutine Can And Cannot Hand Back: ### What A Subroutine Can And Cannot Hand Back:
This is the thing that catches people, including whoever wrote the last three pieces of system code, so it is worth stating once and plainly. This is the thing that catches people, including whoever wrote the last three pieces of system code, so it is worth stating once and plainly.
+469
View File
@@ -0,0 +1,469 @@
; config.asm
; Reading a configuration file.
;
; One setting to a line: a key, a space, and the rest of the line is the value. A semicolon
; starts a comment and a blank line is nothing. That format was not designed so much as
; noticed - textSplit already cuts the first word off a line and leaves the rest, and
; textSame already compares two strings and insists they end together, so reading a setting
; is those two routines and a loop. It is also the shape the shell reads, which means a
; configuration line is a command line the machine reads instead of a person typing one.
;
; ---- Configuration is advice ----
;
; A file that is not there, a key that is not in it, and a value that makes no sense are
; all the same answer: use the default. Nothing here reports a failure for any of them,
; because a program that cannot run without its configuration has turned its configuration
; into a single point of failure - and for the thing that starts the machine, that would
; mean a mistyped file is a machine that does not start.
;
; ---- But quiet is not the same as silent ----
;
; A setting that was meant and did not take effect should say so, or the only symptom is
; that the machine did not do what somebody asked it to. So there are TWO routines rather
; than one: cfgGet reads, and never says anything; cfgCheck reads the whole file once and
; reports what it did not understand.
;
; They are separate because they know different things. A malformed line is malformed to
; anybody, so cfgGet's own scan could spot one - but reading three settings would then
; report the same bad line three times. And an unknown KEY is not something this can judge
; at all: only the caller knows which keys mean anything to it, which is why cfgCheck is
; given a table of them.
;
; Written by Anachronaut
#Program
; ---- Reading the file in ----
;
; DP0 names a path, DP1 names a buffer, and A is how many blocks the buffer holds.
;
; Q is zero if there is something to read, INCLUDING WHEN THERE IS NO FILE. A missing
; configuration file is a file with no settings in it, which is a perfectly ordinary thing
; for a disk to have, and the caller wanting the defaults gets them either way.
cfgLoad:
SETD.2 CfgRoom
STA.2
SETD.2 CfgBufferAt
STD.1.2
; Nothing loaded until something is.
RSTA
SETD.2 CfgLength
STA.2
INCD.2
STA.2
CALL sbfsFind
BNQ cfgNoFile
; A file too big for the buffer is read as far as it fits rather than refused: the
; settings at the top still work, and the ones past the end are missing keys, which is
; a case every caller already handles.
SETD.2 SbfsFileBlocks
INCD.2
LDA.2
SETD.2 CfgRoom
LDB.2
CCF
SUB
BNC cfgTooBig
SETD.2 CfgBufferAt
LDD.1.2
CALL sbfsRead
BNQ cfgNoFile
; How many bytes of it are real. A file is whole blocks and then a tail, which is the
; block count in the high byte and the tail in the low one.
SETD.2 SbfsFileBlocks
INCD.2
LDA.2
SETD.2 CfgLength
STA.2
SETD.2 SbfsFileTail
LDA.2
SETD.2 CfgLength
INCD.2
STA.2
cfgReady:
RSTA
RSTB
CCF
ADD
RET
cfgTooBig:
; As much of it as there is room for, which is every whole block of the buffer.
SETD.2 CfgRoom
LDA.2
SETD.2 SbfsFileBlocks
INCD.2
STA.2
RSTA
SETD.2 SbfsFileTail
STA.2
SETD.2 CfgBufferAt
LDD.1.2
CALL sbfsRead
BNQ cfgNoFile
SETD.2 CfgRoom
LDA.2
SETD.2 CfgLength
STA.2
RSTA
INCD.2
STA.2
BRI cfgReady
cfgNoFile:
; No file, or a disk that would not give it up. Neither is a failure: it is a file with
; nothing in it, and every key will be missing, which is what a default is for.
RSTA
SETD.2 CfgLength
STA.2
INCD.2
STA.2
BRI cfgReady
; ---- Walking it a line at a time ----
;
; cfgRewind puts the walk back at the top. cfgLine copies the next line into CfgLine and
; leaves Q zero if there was one.
;
; A line longer than the buffer holds is copied as far as it goes and the rest of it is
; skipped, with CfgLong set to say so. Ignoring it is what the format asks for; saying that
; it was ignored is what cfgCheck is for.
cfgRewind:
SETD.0 CfgBufferAt
SETD.1 CfgWalk
CALL sbfsCopyWord
SETD.0 CfgLength
SETD.1 CfgLeft
CALL sbfsCopyWord
RET
cfgLine:
RSTA
SETD.0 CfgLong
STA.0
SETD.0 CfgFill
STA.0
SETD.0 CfgLeft
LDA.0
INCD.0
LDB.0
OR
BRQ cfgLineNone
SETD.2 CfgWalk
LDD.0.2
SETD.1 CfgLine
cfgLineLoop:
SETD.2 CfgLeft
LDA.2
INCD.2
LDB.2
OR
BRQ cfgLineEnd
LDA.0
INIB 0x0A
CCF
SUB
BRQ cfgLineBreak
; Room for it, or the line is one of the long ones and only its beginning is kept.
SETD.2 CfgFill
LDB.2
INIA 0d128
CCF
SUB
BRQ cfgLineOverflow
LDA.0
STA.1
INCD.1
LDA.2
INCA
STA.2
cfgLineStep:
INCD.0
RCAL cfgSpent
BRI cfgLineLoop
cfgLineOverflow:
INIA 0x01
SETD.2 CfgLong
STA.2
BRI cfgLineStep
cfgLineBreak:
INCD.0
RCAL cfgSpent
cfgLineEnd:
RSTA
STA.1 ; The zero that makes what was copied a string.
SETD.2 CfgWalk
STD.0.2
RSTA
RSTB
CCF
ADD
RET
cfgLineNone:
RSTA
INIB 0d1
CCF
ADD
RET
; One byte of the file accounted for.
cfgSpent:
SETD.2 CfgLeft
INCD.2
LDA.2
BNA cfgSpentLow
SETD.2 CfgLeft
LDA.2
DECA
STA.2
INCD.2
INIA 0xFF
STA.2
RRET
cfgSpentLow:
DECA
STA.2
RRET
; ---- What a line turns out to be ----
;
; Q is zero if there is a setting on it, and then CfgLine names the key and CfgValue names
; the value. Q is not zero for a blank line or a comment, which are not settings and are
; not mistakes either.
cfgParse:
RSTA
SETD.0 CfgNoValue
STA.0
; A LINE THAT WAS CUT SHORT IS NOT A SETTING. Its key may look perfectly good and its
; value is whatever fitted, so treating it as one would hand back an answer that is wrong
; rather than missing - and a missing setting gets the default, which is the safe thing.
; Reported by cfgCheck, ignored by everything.
SETD.0 CfgLong
LDA.0
BNA cfgParseNothing
SETD.0 CfgLine
LDA.0
BRA cfgParseNothing
INIB 0x3B ; A semicolon starts a comment.
CCF
SUB
BRQ cfgParseNothing
CALL textSplit
SETD.0 TextRest
SETD.1 CfgValue
CALL sbfsCopyWord
; A KEY WITH NOTHING AFTER IT IS NOT A SETTING, and finding that out here rather than in
; each caller is what makes first-match-wins safe. A file with
;
; system
; system /System/Boot/cosmos.bin
;
; in it would otherwise match the first line, hand back an empty value, and the machine
; would try to start a file with no name while a perfectly good setting sat underneath.
; An unusable value is an absent one, which is what C3 says and where it earns its keep.
SETD.2 CfgValue
LDD.0.2
LDA.0
BRA cfgParseNoValue
RSTA
RSTB
CCF
ADD
RET
cfgParseNoValue:
; Told apart from a blank line, because one of them is a mistake and the other is not.
INIA 0x01
SETD.0 CfgNoValue
STA.0
cfgParseNothing:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Asking for one setting ----
;
; DP0 names the key. Q is zero if the file had it, and CfgValue then names the value.
; Says nothing about anything, ever.
cfgGet:
SETD.1 CfgWanted
STD.0.1
CALL cfgRewind
cfgGetLoop:
CALL cfgLine
BNQ cfgGetMissing
CALL cfgParse
BNQ cfgGetLoop
SETD.0 CfgLine
SETD.2 CfgWanted
LDD.1.2
CALL textSame
BNQ cfgGetLoop
RSTA
RSTB
CCF
ADD
RET
cfgGetMissing:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Saying what was not understood ----
;
; DP0 names a table of the keys the caller knows: strings one after another, ended by an
; empty one. Every line is looked at once, and anything that will not take effect is
; reported with enough of itself to be found and fixed.
;
; Nothing here changes what any setting does. The defaults have already been used by the
; time this runs, or will be; this exists so that a setting somebody meant, which did not
; happen, does not do so in silence.
cfgCheck:
SETD.1 CfgKnown
STD.0.1
CALL cfgRewind
cfgCheckLoop:
CALL cfgLine
BNQ cfgCheckDone
SETD.0 CfgLong
LDA.0
BRA cfgCheckParse
SETD.0 CfgLongText
RCAL cfgSay
SETD.0 CfgLine
RCAL cfgSay
RCAL cfgNewLine
BRI cfgCheckLoop ; Said once. What its key looks like is not worth a second remark.
cfgCheckParse:
CALL cfgParse
BRQ cfgCheckKnown
; Not a setting. A blank line or a comment is not a mistake and gets no remark; a key
; somebody started and did not finish is, and gets one.
SETD.0 CfgNoValue
LDA.0
BRA cfgCheckLoop
SETD.0 CfgEmptyText
RCAL cfgSay
SETD.0 CfgLine
RCAL cfgSay
RCAL cfgNewLine
BRI cfgCheckLoop
cfgCheckKnown:
SETD.2 CfgKnown
LDD.1.2
cfgCheckNext:
LDA.1
BRA cfgCheckUnknown ; The empty name that ends the table.
SETD.0 CfgLine
CALL textSame
BRQ cfgCheckLoop ; A key this caller knows, so there is nothing to say.
; Past this name and on to the next one.
cfgCheckSkip:
LDA.1
BRA cfgCheckSkipped
INCD.1
BRI cfgCheckSkip
cfgCheckSkipped:
INCD.1
BRI cfgCheckNext
cfgCheckUnknown:
SETD.0 CfgUnknownText
RCAL cfgSay
SETD.0 CfgLine
RCAL cfgSay
RCAL cfgNewLine
BRI cfgCheckLoop
cfgCheckDone:
RET
; ---- Saying things ----
;
; Its own rather than the console's, because the first thing to read a configuration file
; is the boot loader and everything it uses has to fit in a boot slot.
cfgSay:
LDA.0
BRA cfgSaid
OUTA 0x00
INCD.0
BRI cfgSay
cfgSaid:
RRET
cfgNewLine:
INIA 0x0A
OUTA 0x00
RRET
#Data
CfgLongText:
"a line too long to read: "
CfgEmptyText:
"a setting with no value: "
CfgUnknownText:
"a setting nothing asked for: "
CfgBufferAt:
0x00 0x00
CfgRoom:
0x00
CfgLength:
0x00 0x00
CfgWalk:
0x00 0x00
CfgLeft:
0x00 0x00
CfgFill:
0x00
CfgLong:
0x00
CfgNoValue:
0x00
CfgWanted:
0x00 0x00
CfgKnown:
0x00 0x00
CfgValue:
0x00 0x00
; One line, and the number is the one the format says: a key of twenty two and a path of
; the length a path is allowed to be leaves room to spare in a hundred and twenty eight.
CfgLine:
#Reserve 0d129
File diff suppressed because it is too large Load Diff
+141
View File
@@ -0,0 +1,141 @@
; Read a named file from beginning to end, one block at a time.
;
; DP0 = filename
; CALL fileStreamOpen Q = 0, or the osFileInfo error
; CALL fileStreamNext Q = 0, or the osFileBlock error
; DP3 = bytes in FileStreamBlock; zero means EOF
;
; This is application-side machinery built on CosmOS services, not a filesystem and not
; yet an OS stream. Open remembers the address of the caller's filename, which must remain
; valid until the one stream is finished.
;
; Written by ChatGPT for Anachronaut's SplitBit
#Program
fileStreamOpen:
SETD.3 FileStreamName
STD.0.3
SWI osFileInfo
BNQ fileStreamReturn
PSHD.3
POPB
POPA
SETD.3 FileStreamBlocks
STA.3
INCD.3
STB.3
SETD.3 FileStreamIndex
RSTA
STA.3
INCD.3
STA.3
fileStreamReturn:
RET
fileStreamNext:
SETD.3 FileStreamBlocks
LDA.3
INCD.3
LDB.3
OR
BRQ fileStreamEnd
SETD.3 FileStreamName
LDD.0.3
SETD.1 FileStreamBlock
SETD.3 FileStreamIndex
LDA.3
INCD.3
LDB.3
SWI osFileBlock
BNQ fileStreamReturn
; DP3 is the service's return value and also the pointer this routine uses for all of
; its bookkeeping. Keep the value before touching the pointer, then restore it at the
; one successful return below.
PSHD.3
POPB
POPA
SETD.3 FileStreamCount
STA.3
INCD.3
STB.3
; Index++.
SETD.3 FileStreamIndex
INCD.3
LDA.3
INCA
STA.3
BNC fileStreamTakeBlock
DECD.3
LDA.3
INCA
STA.3
fileStreamTakeBlock:
; Blocks--.
SETD.3 FileStreamBlocks
INCD.3
LDA.3
BRA fileStreamBlocksBorrow
DECA
STA.3
BRI fileStreamReadReturn
fileStreamBlocksBorrow:
INIA 0xFF
STA.3
DECD.3
LDA.3
DECA
STA.3
fileStreamReadReturn:
SETD.3 FileStreamCount
LDD.3.3
RET
fileStreamEnd:
; There is no reset-pointer instruction. Two zero bytes through the Stack are the
; literal construction of a zero Data Pointer.
RSTA
PSHA
PSHA
POPD.3
RET
; DP2 names a big-endian sixteen-bit byte count. Decrement it, returning Q = 0 when it
; reached zero and nonzero while bytes remain. This is shared because correctly counting
; a full 0x0100-byte block is the least interesting part of both applications to duplicate.
fileStreamTakeRemaining:
INCD.2
LDA.2
BRA fileStreamRemainingBorrow
DECA
STA.2
DECD.2
LDB.2
INCD.2
LDA.2
OR
RET
fileStreamRemainingBorrow:
INIA 0xFF
STA.2
DECD.2
LDA.2
DECA
STA.2
LDB.2
INCD.2
LDA.2
OR
RET
#Data
FileStreamName:
0x00 0x00
FileStreamBlocks:
0x00 0x00
FileStreamIndex:
0x00 0x00
FileStreamCount:
0x00 0x00
FileStreamBlock:
#Reserve 0d256
File diff suppressed because it is too large Load Diff
+73
View File
@@ -68,6 +68,47 @@
; 4 the disk would not read it (osFileBlock only) ; 4 the disk would not read it (osFileBlock only)
osFileInfo 0d26 ; DP0 names it. Q is zero if it is there, DP3 is how many blocks. osFileInfo 0d26 ; DP0 names it. Q is zero if it is there, DP3 is how many blocks.
osFileBlock 0d27 ; DP0 names it, DP1 says where, A and B are which block from zero. osFileBlock 0d27 ; DP0 names it, DP1 says where, A and B are which block from zero.
; ---- Moving about ----
;
; DP0 names a directory. Q is zero if the machine is now in it.
;
; WHAT A PROGRAM CHANGES HERE, THE SHELL PUTS BACK when the program stops - the same
; discipline the Stack and the vector table are held to, and for the same reason. A program
; is entitled to move about; the shell is entitled to find itself where it left off.
;
; This is what makes a bare name mean something to a program: everything a program opens is
; relative to here, so a program given a directory to work in can say "notes.txt" and mean
; the one in it.
osChangeDir 0d28
; ---- Writing a file a block at a time ----
;
; The mirror of osFileInfo and osFileBlock, and the way to write something too big to hold
; in memory. osFileSave stays for a whole document handed over at once, which is what a
; text editor has and what most programs want.
;
; ONE WRITE IS OPEN AT A TIME AND THE SYSTEM 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 must 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 that already exists is touched until osFileDone, so a disk without room says so
; while the old file is still there.
;
; osFileStart is told the size the way an entry holds one, blocks and a tail, rather than a
; count of bytes - so it reaches the whole disk. osFileSave is handed a byte count in two
; registers and cannot write more than 65,535.
osFileStart 0d29 ; DP0 names it, DP3 is whole blocks, A is bytes in the tail.
osFileWrite 0d30 ; DP1 is the block, A and B together are which one, from zero.
osFileDone 0d31 ; DP3 is whole blocks and A the tail: how big it turned out to be.
osFileFetch 0d32 ; DP1 is where it goes, A and B are which block. Reads one back.
; osFileFetch is what lets a program keep only ONE block of a file in hand while writing
; it. Anything producing two parts of a file at once - an assembler, whose source says
; #Program and #Data in whatever order it likes - has to be able to put a block down, go
; and write somewhere else, and pick it up again where it left off.
; Q is zero if it read, DP3 is how many of its bytes are the file's: ; Q is zero if it read, DP3 is how many of its bytes are the file's:
; a whole 0d256 except in a last block that is short. That count is ; a whole 0d256 except in a last block that is short. That count is
; why DP3 answers and not a register - 0d256 does not fit in a byte, ; why DP3 answers and not a register - 0d256 does not fit in a byte,
@@ -94,3 +135,35 @@
; restore. The price is that it is part of the program: a build with breakpoints in it has ; restore. The price is that it is part of the program: a build with breakpoints in it has
; different addresses from one without. ; different addresses from one without.
osBreak 0d25 osBreak 0d25
; ---- How the last start went ----
;
; The loader marks the disk before it hands over and the system clears the mark on reaching
; its prompt, so a mark still set is a start that never arrived. See the boot state in
; sbfs.h for what the numbers mean.
;
; osBootState answers in Q: 0 settled, 1 trying, 2 fell back. A machine with no disk answers
; settled, because there is nothing there to be unsettled about.
;
; osBootSettle puts it back to settled, which is how a machine that fell back is told the
; situation has changed. Q is zero if the disk took it.
;
; THE ONLY WRITE A PROGRAM GETS IS SETTLING. 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.
osBootState 0d33
osBootSettle 0d34
; ---- What a program made of it ----
;
; osExit takes a status in A: zero if the program did what it was asked, and a number of
; its own choosing if it did not. 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
; does, and 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 and one in A.
;
; osLastStatus answers in Q with what the last program exited with. The shell does not
; print it: a program that failed has already said so in words, and a number beside that
; would be noise. This is for the thing that cannot read words.
osLastStatus 0d35
+1 -3
View File
@@ -94,8 +94,7 @@ textHexWord:
textHexLoop: textHexLoop:
LDA.0 LDA.0
CALL textHexDigit CALL textHexDigit
PSHQ MVQA
POPA
INIB 0xFF INIB 0xFF
CCF CCF
SUB SUB
@@ -273,7 +272,6 @@ textNumberLoop:
POPB POPB
CCF CCF
ADD ; And the digit. ADD ; And the digit.
SETD.1 TextValue
STQ.1 STQ.1
SETD.1 TextDigits SETD.1 TextDigits
@@ -5,7 +5,7 @@
#Program #Program
start: start:
; Load our initial values into A and B. ; Load our initial values into A and B.
INIA 0x00 RSTA
CALL printByteDecimal CALL printByteDecimal
CALL blankSpace CALL blankSpace
; Move the value into B. ; Move the value into B.
@@ -22,8 +22,7 @@ start:
PSHA PSHA
POPB POPB
; Copy Q into A ; Copy Q into A
PSHQ MVQA
POPA
; Print A. ; Print A.
CALL printByteDecimal CALL printByteDecimal
CALL blankSpace CALL blankSpace
+7 -14
View File
@@ -193,50 +193,43 @@ countNeighbors:
LDB LDB
CCF CCF
ADD ADD
PSHQ MVQA
POPA
DPUP 0d02 DPUP 0d02
LDB LDB
CCF CCF
ADD ADD
PSHQ MVQA
POPA
DPUP 0d02 DPUP 0d02
LDB LDB
CCF CCF
ADD ADD
PSHQ MVQA
POPA
DPUP 0d32 DPUP 0d32
LDB LDB
CCF CCF
ADD ADD
PSHQ MVQA
POPA
DPUP 0d04 DPUP 0d04
LDB LDB
CCF CCF
ADD ADD
PSHQ MVQA
POPA
DPUP 0d32 DPUP 0d32
LDB LDB
CCF CCF
ADD ADD
PSHQ MVQA
POPA
DPUP 0d02 DPUP 0d02
LDB LDB
CCF CCF
ADD ADD
PSHQ MVQA
POPA
DPUP 0d02 DPUP 0d02
LDB LDB
+1 -1
View File
@@ -20,7 +20,7 @@ start:
INCD ; Increment to the next spot in the buffer. INCD ; Increment to the next spot in the buffer.
BRI inputLoop ; Loop again to grab more string. BRI inputLoop ; Loop again to grab more string.
inputEnd: inputEnd:
INIA 0x00 ; Set A to 0. RSTA ; Set A to 0.
STA ; Store it in the buffer. STA ; Store it in the buffer.
SETD Buffer ; Set the Data Pointer to the start of the buffer again. SETD Buffer ; Set the Data Pointer to the start of the buffer again.
CALL printString ; Print it out. CALL printString ; Print it out.
+17 -27
View File
@@ -18,8 +18,7 @@ repl:
CALL printString CALL printString
CALL readNonSpace CALL readNonSpace
PSHQ MVQA
POPA
; Q, q, or end-of-file exits. ; Q, q, or end-of-file exits.
INIB 0xFF INIB 0xFF
@@ -39,14 +38,12 @@ repl:
BRQ inputError BRQ inputError
CALL readNonSpace CALL readNonSpace
PSHQ MVQA
POPA
SETD Operator SETD Operator
STA STA
CALL readNonSpace CALL readNonSpace
PSHQ MVQA
POPA
CALL readHexByteFirst CALL readHexByteFirst
SETD RightOperand SETD RightOperand
STQ STQ
@@ -98,7 +95,7 @@ readNonSpaceLoop:
INIB 0x0D INIB 0x0D
XOR XOR
BRQ readNonSpaceLoop BRQ readNonSpaceLoop
INIB 0x00 RSTB
CCF CCF
ADD ADD
RET RET
@@ -108,12 +105,11 @@ readNonSpaceLoop:
readHexByteFirst: readHexByteFirst:
CALL clearParseStatus CALL clearParseStatus
CALL hexNibble CALL hexNibble
PSHQ MVQA
POPA
INIB 0xFF INIB 0xFF
XOR XOR
BRQ invalidByte BRQ invalidByte
INIB 0x00 RSTB
SHL SHL
SHL SHL
SHL SHL
@@ -121,11 +117,9 @@ readHexByteFirst:
PSHA PSHA
CALL readNonSpace CALL readNonSpace
PSHQ MVQA
POPA
CALL hexNibble CALL hexNibble
PSHQ MVQA
POPA
INIB 0xFF INIB 0xFF
XOR XOR
BRQ invalidLowNibble BRQ invalidLowNibble
@@ -142,7 +136,7 @@ invalidByte:
STA STA
POPD POPD
INIA 0xFF INIA 0xFF
INIB 0x00 RSTB
CCF CCF
ADD ADD
RET RET
@@ -157,8 +151,7 @@ hexNibble:
CCF CCF
SUB SUB
BRC tryUpperHex BRC tryUpperHex
PSHQ MVQA
POPA
INIB 0d10 INIB 0d10
CCF CCF
SUB SUB
@@ -171,8 +164,7 @@ tryUpperHex:
CCF CCF
SUB SUB
BRC tryLowerHex BRC tryLowerHex
PSHQ MVQA
POPA
INIB 0d06 INIB 0d06
CCF CCF
SUB SUB
@@ -185,8 +177,7 @@ tryLowerHex:
CCF CCF
SUB SUB
BRC badNibble BRC badNibble
PSHQ MVQA
POPA
INIB 0d06 INIB 0d06
CCF CCF
SUB SUB
@@ -195,14 +186,14 @@ tryLowerHex:
badNibble: badNibble:
POPA POPA
INIA 0xFF INIA 0xFF
INIB 0x00 RSTB
CCF CCF
ADD ADD
RET RET
decimalNibble: decimalNibble:
POPB POPB
INIB 0x00 RSTB
CCF CCF
ADD ADD
RET RET
@@ -223,8 +214,7 @@ lowerNibble:
; Return Q=0 if Q was 0xFF, otherwise return a nonzero value. ; Return Q=0 if Q was 0xFF, otherwise return a nonzero value.
resultIsInvalid: resultIsInvalid:
PSHQ MVQA
POPA
INIB 0xFF INIB 0xFF
XOR XOR
RET RET
@@ -288,7 +278,7 @@ evaluate:
INIA 0x01 INIA 0x01
STA STA
RSTA RSTA
INIB 0x00 RSTB
CCF CCF
ADD ADD
RET RET
@@ -360,7 +350,7 @@ multiplyLoop:
multiplyDone: multiplyDone:
SETD Product SETD Product
LDA LDA
INIB 0x00 RSTB
CCF CCF
ADD ADD
RET RET
+4 -8
View File
@@ -25,8 +25,7 @@ int8mult:
DECA ; Subtract one from the multiplier. DECA ; Subtract one from the multiplier.
BRA int8multDone ; If the multiplier becomes zero, we're done. BRA int8multDone ; If the multiplier becomes zero, we're done.
PSHA ; Push the multiplier back to the stack. PSHA ; Push the multiplier back to the stack.
PSHQ ; Push the running total to the stack. MVQA ; Push the running total to the stack. Pop the running total into A.
POPA ; Pop the running total into A.
BRI int8multLoop BRI int8multLoop
return0: return0:
RSTA RSTA
@@ -58,8 +57,7 @@ int8div:
POPA ; Pop the quotient counter from the stack. POPA ; Pop the quotient counter from the stack.
INCA ; Increment it. INCA ; Increment it.
PSHA ; Push it back onto the stack. PSHA ; Push it back onto the stack.
PSHQ ; Push the running total onto the stack. MVQA ; Push the running total onto the stack. Pop the runing total into A.
POPA ; Pop the runing total into A.
BRI int8divLoop ; Branch back to the loop. BRI int8divLoop ; Branch back to the loop.
int8divDone: int8divDone:
CCF ; Clear the Carry Flag. CCF ; Clear the Carry Flag.
@@ -76,13 +74,11 @@ int8mod:
int8modLoop: int8modLoop:
SUB ; Subtract B from A. SUB ; Subtract B from A.
BRC int8modDone BRC int8modDone
PSHQ MVQA
POPA
BRI int8modLoop BRI int8modLoop
int8modDone: int8modDone:
; Add the divisor to Q to get the remainder. ; Add the divisor to Q to get the remainder.
CCF CCF
PSHQ MVQA
POPA
ADD ADD
RET RET
+2 -2
View File
@@ -97,10 +97,10 @@ printByteDecimal:
; Expects A to contain the byte you want to print as a hexadecimal value. ; Expects A to contain the byte you want to print as a hexadecimal value.
printByteHex: printByteHex:
INIB 0x00 ; Set B to 0. RSTB ; Set B to 0.
SHR SHR SHR SHR ; Shift Right four times to move the high nybble into the lower half of A. SHR SHR SHR SHR ; Shift Right four times to move the high nybble into the lower half of A.
CALL printHexDigit ; Print the nybble. CALL printHexDigit ; Print the nybble.
INIA 0x00 ; Set A to 0. RSTA ; Set A to 0.
SHL SHL SHL SHL ; Shift left four times to move the low nybble back into the lower half of A. SHL SHL SHL SHL ; Shift left four times to move the low nybble back into the lower half of A.
CALL printHexDigit ; Print the nybble. CALL printHexDigit ; Print the nybble.
RET ; Return to the caller. RET ; Return to the caller.
+106 -34
View File
@@ -79,6 +79,23 @@ DISKTOOL ?= ../SplitDisk
COSMOS = $(BUILD)/CosmOS/Source/cosmos.bin COSMOS = $(BUILD)/CosmOS/Source/cosmos.bin
APPS = $(patsubst CosmOS/Apps/%.asm,$(BUILD)/CosmOS/Apps/%.sbx,$(wildcard CosmOS/Apps/*.asm)) APPS = $(patsubst CosmOS/Apps/%.asm,$(BUILD)/CosmOS/Apps/%.sbx,$(wildcard CosmOS/Apps/*.asm))
COSMOS_DISK = $(BUILD)/cosmos.img COSMOS_DISK = $(BUILD)/cosmos.img
# ---- What starts the machine ----
#
# Stage two goes into a boot slot as RAW BYTES: stage one reads blocks into Program Memory
# and jumps to the first one, so a sixteen byte header would be sixteen bytes of nonsense
# executed first. Its Data Segment travels with it and it copies that down itself.
#
# Stage one is not here at all. It is the ROM, built into the emulator by the top level
# makefile from the same source, which is what makes it the one part of this that a disk
# cannot replace.
STAGE2 = $(BUILD)/Boot/stage2.raw
DEPENDENCIES += $(BUILD)/Boot/stage2.d
$(STAGE2): Boot/stage2.asm
@mkdir -p $(@D)
$(ASM) $(INCLUDES) -M $(BUILD)/Boot/stage2.d -o $(BUILD)/Boot/stage2.sbx $<
tail -c +17 $(BUILD)/Boot/stage2.sbx > $@
DEPENDENCIES += $(APPS:.sbx=.d) DEPENDENCIES += $(APPS:.sbx=.d)
$(BUILD)/CosmOS/Apps/%.sbx: CosmOS/Apps/%.asm $(BUILD)/CosmOS/Apps/%.sbx: CosmOS/Apps/%.asm
@@ -100,52 +117,107 @@ cosmos: $(COSMOS) $(APPS) $(NATIVE_ASM)
# Made from scratch every time, so that what is on it is what is in Apps/ now and not # Made from scratch every time, so that what is on it is what is in Apps/ now and not
# also whatever used to be. # also whatever used to be.
$(COSMOS_DISK): $(APPS) $(NATIVE_ASM) testPrograms/stringKeyword.asm \ #
CosmOS/Apps/hello.asm CosmOS/Apps/Say.asm CosmOS/Apps/Keys.asm \ # TWENTY FOUR DIRECTORY BLOCKS, WHICH IS ONE HUNDRED AND NINETY TWO NAMES. It was eight,
CosmOS/Source/services.asm CosmOS/Source/console.asm \ # and that is sixty four, of which thirty nine were already spoken for. The two ceilings a
# disk has were nowhere near each other: at the average file on here, twenty six blocks,
# sixty four names run out with the disk forty one per cent full. Names were going to be
# gone long before space was.
#
# A directory block is 256 bytes and holds eight entries, so the difference costs sixteen
# blocks of four thousand and ninety six - three tenths of one per cent - to buy a hundred
# and twenty eight more names. The superblock has carried this number per disk since the
# format was written, so nothing but this line knows what it is.
$(COSMOS_DISK): $(APPS) $(NATIVE_ASM) $(COSMOS) $(STAGE2) testPrograms/stringKeyword.asm \
$(wildcard CosmOS/Apps/*.asm) \
$(wildcard CosmOS/Source/*.asm) $(wildcard CosmOS/Assembler/*.asm) $(wildcard CosmOS/Source/*.asm) $(wildcard CosmOS/Assembler/*.asm)
@mkdir -p $(@D) @mkdir -p $(@D)
rm -f $@ rm -f $@
$(DISKTOOL) format $@ 4096 8 @# A BOOT AREA, so that this is a disk the machine can start itself from rather than
@for app in $(APPS); do $(DISKTOOL) put $@ $$app; done @# one it has to be handed. Forty blocks a slot and two slots: stage two is about
$(DISKTOOL) put $@ $(NATIVE_ASM) @# eight thousand bytes, and the second slot is what makes replacing it survivable,
@# SOURCE goes on as well, because an assembler with nothing to assemble is a @# since raw blocks have no name and so nothing to rename.
@# demonstration of nothing. $(DISKTOOL) format $@ 4096 24 40
$(DISKTOOL) boot $@ $(STAGE2) 0
@# THREE DIRECTORIES, WHICH IS WHAT A CLEAN INSTALL LOOKS LIKE: what you run, what you
@# assemble, and what those include. It was thirty nine files in one list with
@# cosmos.asm sitting between fileStream.asm and sbfs.asm.
@# @#
@# hello.asm and Say.asm are the APPLICATION versions, so each assembles to a .sbx @# The split is by ROLE rather than by which directory the host keeps them in. /Source
@# written straight over the one the host tool put there - which means the next @# holds the things you name to the assembler and /Lib the things they pull in, which is
@# thing loaded is a program the machine built itself, in the same breath. @# a distinction the host makes with -I and the machine now makes with a search path of
@# its own: an include is looked for beside you and then in /Lib. Without that, every
@# source that calls a service would have to sit in the same directory as services.asm
@# and there would be nothing to organise.
$(DISKTOOL) mkdir $@ /Apps
$(DISKTOOL) mkdir $@ /Source
$(DISKTOOL) mkdir $@ /Lib
$(DISKTOOL) mkdir $@ /System
$(DISKTOOL) mkdir $@ /System/Boot
@# The system itself, as a file, which is the whole of what boot.cfg chooses between.
@# No boot.cfg is written: stage two falls back to this name when there is none, and a
@# clean install having nothing to configure is the right default.
$(DISKTOOL) put $@ $(COSMOS) /System/Boot/cosmos.bin
@# What you run. /Apps is the second place the shell looks when a word it does not know
@# turns out to be a program, so anything in here starts by name from anywhere.
@for app in $(APPS); do \
$(DISKTOOL) put $@ $$app /Apps/`basename $$app` >/dev/null || exit 1; done
$(DISKTOOL) put $@ $(NATIVE_ASM) /Apps/Asm.sbx
@# What you assemble. All of it, because an assembler with nothing to assemble is a
@# demonstration of nothing:
@# @#
@# Keys.asm is the one that brings a vector of its own, so assembling it exercises the @# > cd /Source
@# version two header and the Vector Segment: the loader installs its handler, the @# /Source> Asm cosmos.asm the system it is running on
@# console interrupts into it, and the shell takes the vector back at exit. @# /Source> Asm Asm.asm and the thing that built it
@#
@# strings.asm is the odd one out on purpose: it has no #Include and no #Base, so it
@# comes out as a boot image rather than a loadable program, and the difference
@# between the two is visible on one disk.
$(DISKTOOL) put $@ CosmOS/Apps/hello.asm
$(DISKTOOL) put $@ CosmOS/Apps/Say.asm
$(DISKTOOL) put $@ CosmOS/Apps/Keys.asm
$(DISKTOOL) put $@ CosmOS/Source/services.asm
$(DISKTOOL) put $@ CosmOS/Source/console.asm
$(DISKTOOL) put $@ testPrograms/stringKeyword.asm strings.asm
@# And the whole of CosmOS, and the whole of the assembler, so that the machine can
@# build the system it is running on and then build the thing that built it:
@#
@# > load Asm.sbx
@# > run cosmos.asm
@# > run Asm.asm
@# @#
@# Both come out byte for byte what the host tool makes from the same source. @# Both come out byte for byte what the host tool makes from the same source.
@for f in CosmOS/Source/*.asm CosmOS/Assembler/*.asm; do \ @#
$(DISKTOOL) put $@ $$f >/dev/null; done @# Keys.asm brings a vector of its own, so assembling it exercises the version two
@# header and the Vector Segment: the loader installs its handler, the console
@# interrupts into it, and the shell takes the vector back at exit.
@#
@# strings.asm is the odd one out on purpose. It has no #Include and no #Base, so it
@# comes out as a boot image rather than a loadable program, and the difference between
@# the two is visible on one disk.
$(DISKTOOL) put $@ CosmOS/Source/cosmos.asm /Source/cosmos.asm
$(DISKTOOL) put $@ CosmOS/Assembler/Asm.asm /Source/Asm.asm
$(DISKTOOL) put $@ CosmOS/Assembler/readTest.asm /Source/readTest.asm
$(DISKTOOL) put $@ CosmOS/Assembler/tokenTest.asm /Source/tokenTest.asm
$(DISKTOOL) put $@ CosmOS/Apps/hello.asm /Source/hello.asm
$(DISKTOOL) put $@ CosmOS/Apps/Say.asm /Source/Say.asm
$(DISKTOOL) put $@ CosmOS/Apps/Keys.asm /Source/Keys.asm
$(DISKTOOL) put $@ testPrograms/stringKeyword.asm /Source/strings.asm
@# And the loader, so the machine can rebuild what starts it. Assembling stage2.asm on
@# the machine and writing the result into the other boot slot is the whole of a
@# self-hosted boot chain, and everything it includes is already in /Lib.
$(DISKTOOL) put $@ Boot/stage1.asm /Source/stage1.asm
$(DISKTOOL) put $@ Boot/stage2.asm /Source/stage2.asm
@# And what those include. Everything here is named by an #Include somewhere and by
@# nothing else, which is exactly what makes it a library rather than a source.
@for f in CosmOS/Source/console.asm CosmOS/Source/fileStream.asm \
CosmOS/Source/sbfs.asm CosmOS/Source/services.asm CosmOS/Source/text.asm \
CosmOS/Source/config.asm \
CosmOS/Assembler/classify.asm CosmOS/Assembler/labels.asm \
CosmOS/Assembler/numbers.asm CosmOS/Assembler/scratch.asm \
CosmOS/Assembler/source.asm CosmOS/Assembler/table.asm \
CosmOS/Assembler/token.asm CosmOS/Assembler/vectors.asm; do \
$(DISKTOOL) put $@ $$f /Lib/`basename $$f` >/dev/null || exit 1; done
# The system as well as the disk. Building only the image leaves whatever cosmos.bin was # The system as well as the disk. Building only the image leaves whatever cosmos.bin was
# there before, or none at all, and then the disk is booted with a system that does not # there before, or none at all, and then the disk is booted with a system that does not
# match the programs on it. # match the programs on it.
cosmos-disk: $(COSMOS) $(COSMOS_DISK) cosmos-disk: $(COSMOS) $(COSMOS_DISK)
run-cosmos: $(COSMOS) $(COSMOS_DISK) # THE MACHINE STARTS ITSELF. No image is named, so the emulator shadows its ROM into
# Program Memory and that reads the disk for everything else - a boot slot, then a loader,
# then whatever /System/Boot/boot.cfg names, or cosmos.bin when it names nothing.
run-cosmos: $(COSMOS_DISK)
$(EMU) --disk $(COSMOS_DISK)
# The same disk with the system handed over directly instead, which is what a debugger
# does: memory is placed from outside and nothing on the disk is consulted about it. Useful
# when the thing being debugged is the boot chain itself, since it skips the boot chain.
run-cosmos-direct: $(COSMOS) $(COSMOS_DISK)
$(EMU) --disk $(COSMOS_DISK) $(COSMOS) $(EMU) --disk $(COSMOS_DISK) $(COSMOS)
clean: clean:
@@ -155,4 +227,4 @@ clean:
# reassembles every program that includes it. # reassembles every program that includes it.
-include $(DEPENDENCIES) -include $(DEPENDENCIES)
.PHONY: all clean cosmos cosmos-disk run-cosmos .PHONY: all clean cosmos cosmos-disk run-cosmos run-cosmos-direct
+1 -1
View File
@@ -179,7 +179,7 @@ refusedHandler:
BRC refusedCarried BRC refusedCarried
RETI RETI
refusedCarried: refusedCarried:
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
+2 -2
View File
@@ -46,7 +46,7 @@ bTaken:
OUTA 0x00 OUTA 0x00
CCF CCF
BNC cTaken BNC cTaken ; splitlint: this test exists to check a branch whose carry is known
BRI wrong BRI wrong
cTaken: cTaken:
CALL blankSpace CALL blankSpace
@@ -78,7 +78,7 @@ cTaken:
INIB 0x01 INIB 0x01
CCF CCF
ADD ADD
BNC wrong BNC wrong ; splitlint: the point is that a set carry does NOT take this
CALL blankSpace CALL blankSpace
INIA 0d99 ; 'c' INIA 0d99 ; 'c'
OUTA 0x00 OUTA 0x00
@@ -102,7 +102,7 @@ stepPastRefusal:
BRC carried BRC carried
RETI RETI
carried: carried:
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
+8 -3
View File
@@ -5,7 +5,12 @@
; the same byte again, which is why this handler moves the saved address on by one ; the same byte again, which is why this handler moves the saved address on by one
; first. MVSD is what lets it reach the frame at all. ; first. MVSD is what lets it reach the frame at all.
; ;
; 0xFE is not an instruction. Writing it as a literal is the only way past the ; CHOSEN AWAY FROM HALT, because the bytes next to it get used. This said 0xFE for
; a long time, and then 0xFE became WAIT - so the test stopped faulting and started
; SLEEPING. It hung instead of failing, which is the worst way for a test to notice
; that the thing it was testing had moved.
;
; 0xFD is not an instruction. Writing it as a literal is the only way past the
; assembler, which is what makes a program containing one buildable. ; assembler, which is what makes a program containing one buildable.
; ;
; Correct output is: ; Correct output is:
@@ -20,7 +25,7 @@ start:
INIA 0x0A INIA 0x0A
OUTA 0x00 OUTA 0x00
0xFE ; Not an instruction. The handler steps over this. 0xFD ; Not an instruction. The handler steps over this.
INIA 0d75 ; 'K'. Reached only because the handler moved the address on. INIA 0d75 ; 'K'. Reached only because the handler moved the address on.
OUTA 0x00 OUTA 0x00
@@ -41,7 +46,7 @@ faultHandler:
RETI RETI
carried: carried:
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
+7 -2
View File
@@ -1,6 +1,11 @@
; Tests that the CPU stops when it meets a byte it cannot decode. ; Tests that the CPU stops when it meets a byte it cannot decode.
; ;
; 0xFE is not an instruction. Placing it in the Program Segment as a literal gets ; CHOSEN AWAY FROM HALT, because the bytes next to it get used. This said 0xFE for
; a long time, and then 0xFE became WAIT - so the test stopped faulting and started
; SLEEPING. It hung instead of failing, which is the worst way for a test to notice
; that the thing it was testing had moved.
;
; 0xFD is not an instruction. Placing it in the Program Segment as a literal gets
; it past the assembler, which is the only way to build a program containing one. ; it past the assembler, which is the only way to build a program containing one.
; ;
; The CPU should raise the Fault Flag, halt, and leave the Program Counter pointing ; The CPU should raise the Fault Flag, halt, and leave the Program Counter pointing
@@ -11,5 +16,5 @@
start: start:
INIA 0d65 ; Something harmless first, so the fault is not at address zero. INIA 0d65 ; Something harmless first, so the fault is not at address zero.
0xFE ; Not an instruction. 0xFD ; Not an instruction.
HALT ; Never reached. HALT ; Never reached.
+1 -1
View File
@@ -143,7 +143,7 @@ guardHandler:
BRC stepCarried BRC stepCarried
RETI RETI
stepCarried: stepCarried:
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
+1 -1
View File
@@ -20,7 +20,7 @@ start:
INCD ; Increment to the next spot in the buffer. INCD ; Increment to the next spot in the buffer.
BRI inputLoop ; Loop again to grab more string. BRI inputLoop ; Loop again to grab more string.
inputEnd: inputEnd:
INIA 0x00 ; Set A to 0. RSTA ; Set A to 0.
STA ; Store it in the buffer. STA ; Store it in the buffer.
SETD Buffer ; Set the Data Pointer to the start of the buffer again. SETD Buffer ; Set the Data Pointer to the start of the buffer again.
CALL printString ; Print it out. CALL printString ; Print it out.
+1 -1
View File
@@ -28,7 +28,7 @@ start:
ADD ; Q = 0, and the Carry Flag is set. ADD ; Q = 0, and the Carry Flag is set.
SIF SIF
CIF CIF
BRC carryHeld BRC carryHeld ; splitlint: whether carry survived SIF and CIF is what is being tested
; Falling through here means one flag trampled the other. ; Falling through here means one flag trampled the other.
INIA 0d88 ; 'X' INIA 0d88 ; 'X'
+7 -1
View File
@@ -114,6 +114,12 @@ Back:
; by hand because SplitBit has no linker: to this program it is just so much data. ; by hand because SplitBit has no linker: to this program it is just so much data.
; ;
; INIA 'l' OUTA 0 INIA 'o' OUTA 0 ... and so on, then RETI ; INIA 'l' OUTA 0 INIA 'o' OUTA 0 ... and so on, then RETI
;
; THESE ARE OPCODES AND THEY MOVE WHEN THE MAP MOVES. Nothing checks them - to the
; assembler they are numbers, and to this program they are data - so an opcode
; reorganisation has to come here by hand. It has happened once: RETI was 0x19 before the
; branch and subroutine blocks were split apart, and this was the only place in the corpus
; that noticed, by faulting on a byte that had stopped being an instruction.
Payload: Payload:
0x26 0x6C 0xD1 0x00 0x26 0x6C 0xD1 0x00
0x26 0x6F 0xD1 0x00 0x26 0x6F 0xD1 0x00
@@ -122,4 +128,4 @@ Payload:
0x26 0x65 0xD1 0x00 0x26 0x65 0xD1 0x00
0x26 0x64 0xD1 0x00 0x26 0x64 0xD1 0x00
0x26 0x0A 0xD1 0x00 0x26 0x0A 0xD1 0x00
0x19 0x73 ; RETI
+2 -2
View File
@@ -8,8 +8,8 @@
#Program #Program
clearRegisters: clearRegisters:
INIA 0x00; RSTA
INIB 0x00; RSTB
ADD ADD
RET RET
+129
View File
@@ -0,0 +1,129 @@
; The six instructions added after the first sixty four, and what each is for.
;
; Two of them are a call that puts nothing back, and four move a Data Pointer by a value
; worked out while the program is running rather than one written into it.
;
; WHY A SECOND KIND OF CALL. CALL puts A, B and Data Pointers 0 through 2 back the way it
; found them, which costs ten bytes of Stack and means a subroutine can only hand anything
; back through Q, DP3 or memory. That is the right default and it is what almost everything
; here uses. RCAL costs two bytes and puts nothing back at all, which is what a short leaf
; routine wants - and it is unsafe in exactly the way its name says, because everything the
; callee touches, the caller has lost.
;
; The two frames are different sizes, so RCAL must be returned from with RRET and CALL with
; RET. Crossing them walks the Stack to somewhere that was never a return address.
;
; WHY OFFSET BY A REGISTER. 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. DPUA and DPDA
; take A; DPUW and DPDW take A and B together, which is how every sixteen bit value on this
; machine is carried between registers.
;
; Written by Anachronaut
#Program
start:
; ---- Offsetting by a byte, up and then back down ----
SETD.0 Text
INIA 0d7
DPUA.0
LDA.0
OUTA 0x00 ; the eighth character
INIA 0d4
DPDA.0
LDA.0
OUTA 0x00 ; and four before it
; ---- Offsetting by a whole sixteen bit value ----
SETD.1 Text
RSTA
INIB 0d13
DPUW.1
LDA.1
OUTA 0x00
RSTA
INIB 0d13
DPDW.1
LDA.1
OUTA 0x00 ; back where it started
; ---- What each kind of call puts back ----
;
; The same callee is reached both ways and sets A to something else. After the raw call
; that is what A holds; after the safe one it is not.
INIA 0x41
RCAL wrecker
OUTA 0x00 ; Z - the callee's
INIA 0x41
CALL polite
OUTA 0x00 ; A - put back
; ---- And what each costs ----
;
; Printed as a digit added to '0', so two bytes reads as "2" and ten reads as the
; character ten along from it.
MVSD.0
PSHD.0
POPB
POPA
SETD.2 Before
STB.2
RCAL rawCost
CALL safeCost
INIA 0x0A
OUTA 0x00
HALT
wrecker:
INIA 0x5A
RRET
polite:
INIA 0x5A
RET
; WRITTEN OUT TWICE RATHER THAN CALLED, because a call would put its own frame down on
; top of the one being measured and each of these would report ten bytes more than it
; costs. The first version of this did exactly that and printed twelve and twenty.
rawCost:
MVSD.1
PSHD.1
POPB
POPA
SETD.2 Before
LDA.2
CCF
SUB
MVQA
INIB 0x30
CCF
ADD
MVQA
OUTA 0x00
RRET
safeCost:
MVSD.1
PSHD.1
POPB
POPA
SETD.2 Before
LDA.2
CCF
SUB
MVQA
INIB 0x30
CCF
ADD
MVQA
OUTA 0x00
RET
#Data
Text:
"0123456789abcdefg"
Before:
0x00
+1 -1
View File
@@ -50,7 +50,7 @@ refusalHandler:
RETI RETI
carried: carried:
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
+1 -1
View File
@@ -114,7 +114,7 @@ bankHandler:
BRC bankCarried BRC bankCarried
RETI RETI
bankCarried: bankCarried:
DPDN.0 0d01 DECD.0
LDA.0 LDA.0
INCA INCA
STA.0 STA.0
+1 -1
View File
@@ -21,7 +21,7 @@
#Program #Program
start: start:
INIA 0x00 RSTA
CALL reportPort CALL reportPort
INIA 0x10 INIA 0x10
CALL reportPort CALL reportPort
+21
View File
@@ -0,0 +1,21 @@
; runOffTest.asm
; A program that runs off the end of itself.
;
; THIS IS WHY 0x00 IS NOT AN INSTRUCTION. Program Memory that has never been written, or a
; load that stopped part way and left zeroes in its tail, used to read as a long run of
; additions - the machine would carry on through them, arrive somewhere unpredictable, and
; whatever went wrong there would be a long way from the byte that caused it.
;
; Nothing in 0x00 to 0x0F is an instruction now, so a run into blank memory faults where it
; is met and says the address. That is the difference between a diagnosis and a search, and
; it costs an opcode block nobody was using.
;
; The program does not halt. It is not supposed to: falling off the end IS the test.
;
; Written by Anachronaut
#Program
start:
INIA 0x2E ; "."
OUTA 0x00
+1 -1
View File
@@ -89,7 +89,7 @@ showFound:
showContents: showContents:
SETD.0 Landing SETD.0 Landing
SETD.1 SbfsFileBlocks SETD.1 SbfsFileBlocks
DPUP.1 0d01 INCD.1
LDA.1 LDA.1
SETD.1 LeftHigh SETD.1 LeftHigh
STA.1 STA.1
+1 -2
View File
@@ -60,13 +60,12 @@ walkCheck:
; A file is its blocks times 256 plus its tail, which is the block count in the high ; A file is its blocks times 256 plus its tail, which is the block count in the high
; byte and the tail in the low one. Nothing has to multiply anything. ; byte and the tail in the low one. Nothing has to multiply anything.
SETD.0 SbfsFileBlocks SETD.0 SbfsFileBlocks
DPUP.0 0d01 INCD.0
LDA.0 LDA.0
SETD.1 Size SETD.1 Size
STA.1 STA.1
SETD.0 SbfsFileTail SETD.0 SbfsFileTail
LDA.0 LDA.0
SETD.1 Size
INCD.1 INCD.1
STA.1 STA.1
@@ -26,6 +26,21 @@
; quiet: 7 a service that says nothing leaves Q as it found it ; quiet: 7 a service that says nothing leaves Q as it found it
; answer: 42 one that does, does not ; answer: 42 one that does, does not
; pointer: ABC and DP3 comes back the same way ; pointer: ABC and DP3 comes back the same way
; sret: 42 and SRET says the same thing without touching the frame
; sretptr: ABC for a pointer too
; sretkept: 7 while everything a RET would restore still comes back
;
; ---- The frame editing above is what SRET exists to replace ----
;
; Everything between MVSD and RETI in "answer" and "pointer" is a routine reaching into its
; own frame to un-save two fields, using offsets it has to know by heart. Thirty places in
; CosmOS did that, and all thirty would have gone quietly wrong the day the frame gained a
; field. SRET is the same instruction sequence as RETI with the saved Q and DP3 stepped
; over instead of restored, so a handler answers the way a subroutine does and nothing
; below has to know what a frame looks like.
;
; Both are kept here on purpose. RETI is still how a HARDWARE handler says it was never
; there, and a service that has nothing to say should still use it.
#Include console.asm #Include console.asm
@@ -59,6 +74,39 @@ start:
POPD.0 POPD.0
CALL printString CALL printString
CALL newLine CALL newLine
; ---- And the same three answers, given with SRET instead ----
SETD.0 SretText
CALL printString
SWI sretAnswer
MVQA
CALL printByteDecimal
CALL newLine
SETD.0 SretPtrText
CALL printString
SWI sretPointer
PSHD.3
POPD.0
CALL printString
CALL newLine
; What SRET must still put back. A handler that trampled A, B and DP0 to DP2 would break
; every caller, which is exactly why RET restores them - so SRET does too, and the number
; printed here is the caller's own, loaded before the service was asked for anything.
INIA 0d7
RSTB
CCF
ADD ; Q is the caller's seven,
SETD.0 SretKeptText ; and DP0 names the label, BEFORE the service is asked.
SWI sretTrample
; Printed after the call rather than before it, which is the whole of the check: the
; handler pointed DP0 somewhere else, so this says "sretkept" only if SRET brought the
; caller's own pointer back. Printing first would have tested nothing at all.
CALL printString
MVQA
CALL printByteDecimal
CALL newLine
HALT HALT
; Says nothing, so whatever the caller had in Q is still there afterwards. ; Says nothing, so whatever the caller had in Q is still there afterwards.
@@ -88,6 +136,31 @@ pointer:
STA.1 STA.1
RETI RETI
; ---- The same answers, without the frame ----
;
; Q and DP3 are set the way any subroutine sets them, and SRET leaves them alone. There is
; no MVSD, no offset, and nothing here that would need revisiting if the frame changed.
sretAnswer:
INIA 0d42
RSTB
CCF
ADD
SRET
sretPointer:
SETD.3 Letters
SRET
; Sets everything a RET would restore to something wrong, to show that SRET restores it.
; If any of these came back, the caller's 7 would not.
sretTrample:
INIA 0d99
INIB 0d98
SETD.0 Letters
SETD.1 Letters
SETD.2 Letters
SRET
#Data #Data
QuietText: QuietText:
@@ -96,6 +169,12 @@ AnswerText:
"answer: " "answer: "
PointerText: PointerText:
"pointer: " "pointer: "
SretText:
"sret: "
SretPtrText:
"sretptr: "
SretKeptText:
"sretkept: "
Letters: Letters:
"ABC" "ABC"
@@ -105,3 +184,6 @@ Letters:
quiet quiet quiet quiet
answer answer answer answer
pointer pointer pointer pointer
sretAnswer sretAnswer
sretPointer sretPointer
sretTrample sretTrample
+1 -1
View File
@@ -24,7 +24,7 @@ start:
; The Stack Pointer points at the next free slot, so the byte pushed last sits one ; The Stack Pointer points at the next free slot, so the byte pushed last sits one
; above it, and the one before that sits two above. ; above it, and the one before that sits two above.
MVSD.0 MVSD.0
DPUP.0 0d01 INCD.0
LDA.0 ; 'K', the last one pushed. LDA.0 ; 'K', the last one pushed.
INCD.0 INCD.0
LDB.0 ; 'O', the one before it. LDB.0 ; 'O', the one before it.
+58
View File
@@ -0,0 +1,58 @@
; waitTest.asm
; WAIT, on a disk slow enough to be worth waiting for.
;
; Two reads, waited for rather than spun on, with INTERRUPTS MASKED and no handler
; installed anywhere. That is the shape this instruction exists for: a program sleeps on a
; device it has no handler for and reads the device's status when it wakes.
;
; THE SECOND READ IS THE TEST. A line that wakes the CPU without being dispatched has to
; be taken down by the WAIT itself; left standing, it is found by the next WAIT, which
; returns at once, and the program spins for the whole of the second read while looking
; exactly as though it slept. Both reads print, so a hang fails here - but a program that
; spun would print the same two characters, and what tells them apart is the emulator's
; count of idle cycles against bus cycles. Tests/terminal.sh is where that is checked,
; because settle() strips cycle counts out of every recorded output including this one.
;
; Written by Anachronaut
#Program
start:
CIF
INIA 0d3
OUTA 0xE3
INIA 0x20
OUTA 0xE2
INIA 0x03
OUTA 0xE8
RSTA
OUTA 0x20
OUTA 0x21
INIA 0x01
OUTA 0x22
CALL waitDisk
INIA 0x31 ; "1"
OUTA 0x00
RSTA
OUTA 0x20
INIA 0x01
OUTA 0x21 ; Block 1, a different one.
INIA 0x01
OUTA 0x22
CALL waitDisk
INIA 0x32 ; "2"
OUTA 0x00
INIA 0x0A
OUTA 0x00
HALT
waitDisk:
INA 0x23
INIB 0x01
AND
BRQ waitDone
WAIT
BRI waitDisk
waitDone:
RET
#Vectors
Boot start
+363 -11
View File
@@ -23,6 +23,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
| [`Source/Emulator`](Source/Emulator) | The machine: CPU, memory controller, devices, console, disk | | [`Source/Emulator`](Source/Emulator) | The machine: CPU, memory controller, devices, console, disk |
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host | | [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem | | [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life | | [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life |
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker | | [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of | | [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
@@ -33,7 +34,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
## The Machine: ## The Machine:
- **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists. - **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists.
- **Its own instruction set**, 64 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself. - **Its own instruction set**, 72 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself.
- **Interrupts.** Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save. - **Interrupts.** Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save.
- **A bus programs can enumerate**, so a program can ask what a machine is made of rather than being told. - **A bus programs can enumerate**, so a program can ask what a machine is made of rather than being told.
- **A memory controller** that reads and writes Program Memory, moves blocks between banks, reaches memory that devices bring with them, and guards a range against being written by accident. It is how a SplitBit machine loads a program. - **A memory controller** that reads and writes Program Memory, moves blocks between banks, reaches memory that devices bring with them, and guards a range against being written by accident. It is how a SplitBit machine loads a program.
@@ -49,7 +50,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
## Getting Started: ## Getting Started:
Clone it and build the three tools. You need gcc and make, or similar: Clone it and build the four tools. You need gcc and make, or similar:
``` ```
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
@@ -89,9 +90,89 @@ Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, o
| `-c`, `--cycles N` | Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time. | | `-c`, `--cycles N` | Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time. |
| `-f`, `--fast` | Run as fast as the host allows, ignoring the emulated cycle rate. | | `-f`, `--fast` | Run as fast as the host allows, ignoring the emulated cycle rate. |
| `-D`, `--disk <file>` | Attach a disk image, creating a 128K one if the file is not there. | | `-D`, `--disk <file>` | Attach a disk image, creating a 128K one if the file is not there. |
| `-L`, `--disk-cycles N` | How many cycles a block read or write takes. Zero, the default, finishes before the next instruction starts. |
| `-W`, `--write-protect` | Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not. | | `-W`, `--write-protect` | Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not. |
| `-h`, `--help` | Show help and usage information. | | `-h`, `--help` | Show help and usage information. |
**The boot image is optional now.** Named one, the emulator places it into memory and
starts it, which is what a debugger does and how every test here runs - a real thing real
machines allow, not a shortcut to apologise for. Given only a disk, the machine starts the
way hardware would:
```
./SplitBit --disk system.img
```
The emulator carries `Programs/Boot/stage1.asm` as a **shadowed ROM**: at reset its bytes
are copied into Program Memory, boot vector included, and the CPU then does exactly what it
has always done - reads the boot vector and starts where it points. Nothing about the CPU
changed to make a machine that starts itself.
Because it is a copy rather than a mapping, those bytes are ordinary Program Memory once
stage one has jumped away. The system may write over them, and a reset puts them back,
which is why rebooting has to be a reset rather than a jump - and `SWI SoftReset` is the
instruction for it.
The ROM is generated from the assembly by the makefile rather than kept beside it, because
a copy of a program stored next to the program is a copy that goes stale.
**A cycle is one access to memory**, not one instruction. Fetching an opcode is a cycle,
fetching each byte after it is another, reading or writing Data Memory is one, every byte a
CALL pushes or a RET pops is one, and reaching a device port is one. Nothing overlaps -
there is no fetching the next instruction while this one finishes - so the count is simply
how many times the machine used the bus.
That makes the numbers describe something buildable. `RSTA` costs 1 and `SETD` costs 4,
because one is a byte and the other is four. `CALL` and `RET` together cost 24 and `RCAL`
and `RRET` cost 8, because the first pair moves twenty bytes of Stack and the second moves
four. Counting instructions said those were the same, which is not true of any machine
anybody could build - and it is the emulator's job to be the thing the hardware is designed
against.
The average SplitBit instruction costs 3.72 cycles, measured over the native assembler
assembling a program.
**The memory controller is charged for what it moves**, on the same terms. Banks are
separate memories, and that is what sets the rate: a move between two of them can overlap
its read and its write, so it settles at a byte a cycle, while a move within one bank cannot
and costs two. A fill has nothing to read and costs one. So a 256 byte block is 257 cycles
between banks and 513 within one, against the ten it used to cost - which was the five port
writes that set it up and nothing for the quarter of a kilobyte that moved.
The transfer stalls the program that asked for it. Whether hardware would let the two run at
once is left open, the same way pipelining is: the memories are separate, so it plausibly
could, and the measurements say it would buy less than it sounds like.
**The disk can be given a latency** with `--disk-cycles`, and then it really does take that
long: it says busy, finishes when the machine has run that far, and a program that does not
wait reads the block *before* the one it asked for. That is not an error anywhere - just
quietly the wrong bytes - which is why the filesystem now watches the busy bit rather than
trusting the answer to be there. Zero is the default and is how the machine has always run.
The waiting is one small routine, and it is reached with `RCAL` rather than `CALL` because
what it hands back is the settled status in A, and an ordinary call would put A back the way
it found it.
**And waiting is not the same kind of cycle as working.** A machine stopped in a `WAIT` is
clocked but is not using the bus, so those cycles are counted apart from the rest and the
halt line says so when there are any:
```
Execution halted after 1042474 cycles, 119772 of them waiting.
```
Added together they are elapsed time, which is what `--cycles` measures. Told apart they
say whether a program was working or waiting - and that distinction is the only thing that
separates a machine which slept through a slow disk from one which spun on it. The two take
the same wall clock time and print the same characters. When the filesystem's wait was
first written, taking the line-clearing out of `WAIT` moved the total by a single cycle,
20,100 against 20,099, while the idle half halved.
Whether real hardware would overlap a fetch with the end of the previous instruction is
left open, and deliberately: this is the conservative model, and pipelining is a decision
to make while drawing the hardware rather than one to inherit from an emulator.
If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed. If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed.
## Assembling: Assembler ## Assembling: Assembler
@@ -105,10 +186,79 @@ If the CPU reads a byte that is not an instruction, it goes to the fault handler
| `-o <file>` | Write the output to this path. | | `-o <file>` | Write the output to this path. |
| `-I <dir>` | Look in this directory for included files. May be given more than once. | | `-I <dir>` | Look in this directory for included files. May be given more than once. |
| `-M <file>` | Write out which source files the output depends on, as a make rule. | | `-M <file>` | Write out which source files the output depends on, as a make rule. |
| `-S <file>` | Write every label and the address it was given, in address order. |
| `-h`, `--help` | Show help and usage information. | | `-h`, `--help` | Show help and usage information. |
`-S` is the only thing that knows what a program's addresses are called. A program on the
disk is bytes; the machine's own monitor can disassemble it but has no idea what any of it
is named. So a count of which addresses get called says a great deal and names nothing, and
this is what turns such a count into a list of routine names.
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`. Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
## Checking Assembly: SplitLint
```
./SplitLint [--fatal-warnings] [--machine] <sourcefile> [sourcefile ...]
```
**Every warning names the rule that produced it**, in brackets the way a compiler names a
flag, and `--machine` prints one tab-separated line per warning and nothing else - file,
line, rule, message, help - so that nothing downstream has to read prose. A run that finds
nothing says so rather than exiting silently, because a tool that says nothing has not told
you it found nothing; it has told you nothing at all, and from outside those look the same:
```
No style warnings: 121 files checked against 12 rules.
```
SplitLint reports valid assembly that has a shorter direct expression, beginning with
zero loads that can use `RSTA` or `RSTB`, Q-to-register transfers that need not pass
through the stack, self-push/pop pairs, register assignments overwritten by the next
instruction, instructions with no ordinary fallthrough path, and one-byte Data Pointer
changes that can use `INCD` or `DECD`, including direct branches whose target is already
the next labeled address. It also tracks symbolic Data Pointer bases and known offsets to
find a `SETD` that reloads an address the pointer already holds. Device input and stack
pops are excluded from dead-assignment checks because consuming their input is itself an effect.
**Nothing it knows survives a call.** `CALL` really does restore A, B and Data Pointers 0
to 2, so a pointer set before one is genuinely still set after it - and saying so produced
advice that was correct today and unsafe to take. 122 of the 178 redundant `SETD`s it first
found were redundant *only* because of that restore, and removing them would have become a
wrong-pointer bug the moment the callee was reached with `RCAL` instead, which is what
`RCAL` was added to this machine for. Worse, the linter would have gone quiet rather than
complained, because it forgets everything across an `RCAL`. Fifty four recommendations that
stay true are worth more than a hundred and seventy eight that are conditional on a change
the project intends to make.
SplitLint's control-flow knowledge is otherwise deliberately local: labels begin reachable regions, while an unparsed
directive or data item ends the current claim. It does not yet expand includes or build a
complete control-flow graph. Warnings normally leave a successful exit status, while
`--fatal-warnings` makes any warning fail the command for use in automated checks.
**A line whose comment says `splitlint: <reason>` is not reported on, and the reason is
required.** A suppression with no explanation is a way to make a tool quiet rather than a
way to say something, and a bare marker is refused rather than honoured. The corpus has
three of them, all in test programs: `branchTest.asm` exists to check that a branch whose
carry is known behaves correctly, so a diagnostic saying the outcome is known is exactly
right and exactly unwanted. `splitlint[rule]: <reason>` silences one rule and leaves the
line honest about the others.
Suppressions are counted and reported at the end of a run, and **a marker that no longer
silences anything is itself reported** - an exception that outlived its reason is the thing
the required reason was meant to prevent.
**The corpus is held to a baseline.** Sixty one warnings are left in it on purpose, and
`Tests/lint-baseline.txt` records how many of each rule each file is expected to produce,
so a sixty second fails `make test` while the sixty one stay quiet. It counts rather than
recording line numbers, because recording lines would churn the whole file whenever
anything was inserted above a warning. `./Tests/lint.sh --bless` records it again once
warnings have been deliberately fixed or deliberately accepted.
The same local model tracks whether carry is known set or clear. It reports a redundant
`CCF`, a `BRC` or `BNC` whose outcome is already determined, and computes carry through
increment, decrement, addition, and subtraction when their inputs are known.
## Managing Disks: SplitDisk ## Managing Disks: SplitDisk
``` ```
@@ -117,16 +267,216 @@ Without `-o` the output takes the source file's name, in the directory you calle
| Command | What it does | | Command | What it does |
| --- | --- | | --- | --- |
| `format <image> [blocks] [dirblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 files. | | `boot <image> <file> [slot]` | Write a file into a boot slot, padding the rest of it with zeroes. Slot 0 unless told otherwise. |
| `list <image>` | Show what is on the disk. | | `bootstate <image> [0\|1\|2]` | Show how the last start went, or set it. Setting it to 0 is how a disk that fell back is told to try again. |
| `put <image> <file> [name]` | Put a host file onto it. Without a name it uses the file's own, which is often longer than the 22 characters a name may be. | | `bootslot <image> <slot>` | Choose which slot the machine starts from. One byte, on its own, so writing a slot and committing to it stay separate decisions. |
| `get <image> <name> [file]` | Take one off it. | | `format <image> [blocks] [dirblocks] [bootblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 entries. A fourth number reserves a boot area of two slots that size. |
| `delete <image> <name>` | Remove one. | | `list <image> [path]` | Show the whole disk, or one directory of it. |
| `put <image> <file> [path]` | Put a host file onto it. Without a path it uses the file's own name, which is often longer than the 22 characters a name may be. |
| `get <image> <path> [file]` | Take one off it. |
| `delete <image> <path>` | Remove a file. |
| `mkdir <image> <path>` | Make a directory. |
| `rmdir <image> <path>` | Remove an empty one. |
### The Boot Area:
Blocks between the superblock and the directory, which the filesystem never allocates and
never sees. **Nothing was added to reserve them.** Both implementations work out the first
usable block as `directoryStart + directoryBlocks`, and `directoryStart` has always been a
field in the superblock rather than a constant, so moving the directory up reserves
everything below it by arithmetic that was already there. Neither allocator changed.
A disk made before any of this has `directoryStart` of 1 and a boot area of zero, which
reads as *not bootable* - true, and the same shape as the version two parent field, where
the value an older disk already held turned out to be the right answer.
**There are always two slots**, because a boot slot is raw blocks. A file being rewritten
is protected by writing a temporary and renaming it, and there is no name here to rename -
so a machine interrupted while updating its only boot slot would not boot at all, which is
the one failure on this disk with no way back. Writing the slot that is *not* live and then
moving one byte in the superblock turns that into a machine that boots what it had before.
### Starting Again:
Writing `1` to port `0x13` asks the machine to start over, and `Reboot` is the program that
does it - forty five bytes, most of them the word it prints.
**A port rather than a service**, because a reset has to work when the system does not.
Something that could only be asked for through `SWI` would be unavailable in exactly the
case that wants it most, and a program that owns the whole machine has no system to ask.
**What a reset repeats is how the machine started.** Named an image, the emulator places it
again; named none, the ROM is shadowed again and reads the disk for the rest. Anything else
would mean a reset changed what the machine *is*, which is the one thing a reset must not
do. It is taken between instructions, because a device cannot restart the machine from
inside the instruction that asked.
The disk is not unplugged and keeps everything written to it. That is what warm means: the
machine starts again, the world it starts into does not. **The vector table is cleared**,
which is the one deliberate departure from leaving memory alone - a vector points into
whatever installed it, and after a reset that program is not running, so a handler left
behind would aim an interrupt at an address belonging to something gone. It is the argument
CosmOS already makes when it takes a program's vectors back at exit.
### Starting Something Else Just This Once:
A program that owns the whole machine has nowhere to run. It cannot be started from the
shell, because starting it means there is no shell; and pointing `boot.cfg` at it means a
machine that keeps starting it, which is a poor place to find a mistake.
```
> Once /System/Boot/mine.bin
next start: /System/Boot/mine.bin, once
```
That writes `/System/Boot/once.cfg`, in the same format as `boot.cfg` and read with the same
routines, because a second format for one setting would be a second format. The loader reads
it before `boot.cfg` and **deletes it before it jumps** - the only moment there is, since
after the jump the loader does not exist.
**Consumed by being read, not by working.** A one shot that hangs cannot hang twice: the
request is gone before the image ran, so the next start reads `boot.cfg` like any other.
And **the boot state is not touched** by a one shot, which the first version got wrong. A
program with the whole machine has no filesystem to clear a mark with, and is doing nothing
wrong by not having one - so marking it reported every successful bare metal boot as a start
that never arrived.
Which closes the loop on the machine itself: write a bare metal program in `Edit`, assemble
it with `Asm`, ask for it with `Once`, restart, watch it run, and the system comes back
without being asked.
### Knowing Whether The Last Start Arrived:
The loader marks the disk before it hands over, and the system clears the mark when it
reaches its prompt. **A system that crashes on the way there leaves the mark**, and the
loader finding it still set next time is how a machine that will not start says so to the
only thing in a position to do anything about it.
| State | Means |
| -- | -- |
| 0 | Settled. The last start arrived, so start what the configuration says. |
| 1 | Trying. The loader handed over and nothing came back to say it got there. |
| 2 | Fell back. A try failed and the fallback was used, and will be until this is settled. |
Three starts of a machine whose configuration names something broken:
```
stage two
a system that never reaches a prompt <- marks the disk, dies
stage two
the last start did not arrive <- finds the mark, uses the fallback
CosmOS
this is the fallback: what boot.cfg asks for did not start
stage two
still on the fallback: settle it to try again <- does NOT retry
```
That third one is the part worth having. A system known not to start is not tried every
other boot for ever; it waits to be told the situation has changed.
**Reaching the prompt is a deliberate choice of threshold.** It is not a claim that the
system works - a shell can be reached by something broken in every other way. It is the
point where somebody can type, which is exactly what the fallback exists to give back:
anything wrong past there can be fixed from the prompt, and nothing wrong before it can be
fixed at all.
`bootBlocks` and `directoryStart` describe the same fact from two sides, so a disk where
they disagree is refused rather than guessed at.
`Programs/Boot/stage1.asm` is what starts a machine from one, and is written to end up in
ROM: **330 bytes**, and everything it knows is a thing that will be true forever. Which
port the disk is on, that a SplitBit disk begins with its own name, and where two numbers
sit in that first block. It does not know what a file is, what a directory is, or that SBFS
has versions - all of that lives in the boot area, on the disk, where it can be replaced.
It reads the live slot into Program Memory, jumps to the first byte, and prints one
character and stops if there is nothing to start.
`Programs/Boot/stage2.asm` is what sits in the slot. It mounts the filesystem, reads
`/System/Boot/boot.cfg` to find out what to start, and takes the image apart: code into Program Memory, data into Data
Memory, and each vector written through the controller, which is the only thing that can
write Program Memory at all. The entry point is noticed on the boot vector's way past,
because Program Memory cannot be read back to look it up afterwards.
**What it loads is an ordinary boot image** - the same SPBT file the emulator has always
been handed directly. That is the whole trick: a second stage that loads the machine's
normal image format is not a boot-specific mechanism, so **bare metal SplitBit stops being
a special case.** A program that wants no operating system under it is just an image,
written under CosmOS like any other, and startable because it is a file.
### Configuration:
One setting to a line: a key, a space, and the rest of the line is the value. A semicolon
starts a comment and a blank line is nothing.
```
; /System/Boot/boot.cfg
system /System/Boot/cosmos.bin
fallback /System/Boot/cosmos-previous.bin
```
That format was noticed rather than designed. `textSplit` already cuts the first word off a
line and leaves the rest; `textSame` already compares two strings and insists they end
together, so `system` and `systemd` are different words. Reading a setting is those two
routines and a loop - and it is the shape the shell already reads, so **a configuration
line is a command line the machine reads instead of a person typing one.**
**Configuration is advice.** A missing file, a missing key, a value that makes no sense, a
line too long to read: all of them mean *use the default*, and none of them is a failure. A
program that cannot run without its configuration has turned its configuration into a
single point of failure, and for the thing that starts the machine that would mean a
mistyped file is a machine that will not start.
**But quiet is not silent.** A setting somebody meant, which did not take effect, says so:
```
a setting nothing asked for: fallbcak
a setting with no value: system
a line too long to read: ...
```
So `config.asm` has two routines rather than one. `cfgGet` reads and never says anything -
reading three settings should not report the same bad line three times. `cfgCheck` reads
the file once and reports what it did not understand, and is given the caller's list of
keys, because whether a key means anything is the only part of this the reader cannot
judge for itself.
Keys are matched exactly and case sensitively, the first line that matches wins, and
unknown keys are ignored rather than refused - so a file written by a newer system loses
the settings an older one never had and keeps the ones it did. A line is at most 128 bytes
and a key at most 22, which is what a name is everywhere else on this machine.
Deliberately absent: sections, nesting, types, includes, substitution, conditions. Each is
a step from reading a file toward running one, and the boot loader is the worst place on
this machine to put an interpreter. The test to apply when the pressure arrives is: **if
this file cannot be read, can the machine still start?**
Stage two arranges its own Data Segment. Stage one places Program Memory and nothing else,
since knowing where a payload's data ended would mean knowing a format, so the image is
written into the slot as code followed by data and stage two's first act is to blit its
data down from just past its own code.
SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. It is a convenience rather than a necessity: SplitBit writes its own filesystem, and now assembles its own programs, so a disk can be filled without leaving the machine. SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. It is a convenience rather than a necessity: SplitBit writes its own filesystem, and now assembles its own programs, so a disk can be filled without leaving the machine.
Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens `put` says so rather than putting part of a file on. Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens `put` says so rather than putting part of a file on.
A path is names with `/` between them, always from the root, since a command line tool has nowhere to keep a working directory between one run and the next. `.` and `..` mean what they usually do, and `..` from the root is the root.
A disk has two ceilings and it is usually the less obvious one that bites: blocks, and **entries**. Every file and every directory costs one entry, and `list` says how many of them are gone as well as how many blocks are. On a disk of small files the entries run out long before the space does, which is a matter of how the disk was formatted rather than a limit of the format - `dirblocks` is carried per disk, and each one is 256 bytes and holds eight entries.
### Two Versions:
| Version | What it means |
| --- | --- |
| 1 | Flat. Every file is in the root, because there is nowhere else. |
| 2 | Directories. Each entry says which directory it is in. |
**A version one disk is already a valid version two disk.** The parent is stored as an entry index *plus one*, so the zeroes a version one disk has in those bytes read as "in the root" - which is exactly where all of its files are. There is nothing to convert.
A disk is at the lowest version that describes what is on it, so `format` makes a version one disk and `mkdir` is what raises it. That is deliberate: a disk stays readable by anything that has never heard of a directory right up until it actually has one. Compatibility runs one way, which is the ordinary shape of it - version one code reading a version two disk would list directories as strange empty files.
## Building Programs With Make: ## Building Programs With Make:
The assembler is built to work with make. `-o` puts the output where the build system wants it, and `-M` writes out which libraries went into it, so that editing a library reassembles everything that includes it. The assembler is built to work with make. `-o` puts the output where the build system wants it, and `-M` writes out which libraries went into it, so that editing a library reassembles everything that includes it.
@@ -170,17 +520,19 @@ The disk images tests read from are built first by `Tests/makedisks.sh`, using S
test that reads one is therefore checked against a filesystem written by different code from test that reads one is therefore checked against a filesystem written by different code from
the same written specification, rather than against itself. the same written specification, rather than against itself.
`Tests/run.sh` drives that comparison. Four more scripts run alongside it, and each exists `Tests/run.sh` drives that comparison. Six more scripts run alongside it, and each exists
because a recorded file cannot answer its question: because a recorded file cannot answer its question:
- **`Tests/disk.sh`** checks the disk tool on its own: files of every awkward size onto an image and off again, and the things the format says cannot happen refused rather than half done. - **`Tests/disk.sh`** checks the disk tool on its own: files of every awkward size onto an image and off again, and the things the format says cannot happen refused rather than half done.
- **`Tests/terminal.sh`** checks what a recorded file cannot see. Piped output is buffered and flushed at exit, so a prompt shown before its answer is asked for and one shown an hour late produce identical files; and key mode only touches a terminal when there is one. Both have gone wrong here, and both were found by a person whose terminal stopped working rather than by anything in this suite. So it runs the emulator under a pseudo-terminal and asks directly: that a prompt arrives before input is read, that a keystroke arrives without Return, that the terminal is handed back however the machine dies, and that suspending and resuming leave it as they found it. - **`Tests/terminal.sh`** checks what a recorded file cannot see. Piped output is buffered and flushed at exit, so a prompt shown before its answer is asked for and one shown an hour late produce identical files; and key mode only touches a terminal when there is one. Both have gone wrong here, and both were found by a person whose terminal stopped working rather than by anything in this suite. So it runs the emulator under a pseudo-terminal and asks directly: that a prompt arrives before input is read, that a keystroke arrives without Return, that the terminal is handed back however the machine dies, and that suspending and resuming leave it as they found it. It also asks the one question about cycles that a recorded file cannot, since the count is stripped from every one: whether a program on a slow disk slept through the wait or spun on it. Both print the same characters and take the same elapsed time, and only the split between idle and bus cycles tells them apart.
- **`Tests/native.sh`** checks the assembler that runs on SplitBit against the one that runs on the host, byte for byte, on a boot image and four loadable programs, and then on CosmOS and on itself, and then on the CosmOS that CosmOS built. - **`Tests/native.sh`** checks the assembler that runs on SplitBit against the one that runs on the host, byte for byte, on a boot image and four loadable programs, and then on CosmOS and on itself, and then on the CosmOS that CosmOS built.
- **`Tests/docs.sh`** checks the manuals against the code: that every instruction has a row and every row is an instruction, that the counts in the headings are right, that every directive is written down, that every service the system implements is described and every service described is implemented, that every routine the manuals promise exists, and that the worked examples still assemble to the bytes printed beside them. - **`Tests/agree.sh`** checks the two implementations of SBFS against each other rather than each against itself, by building the same disk with SplitDisk and with CosmOS and comparing the images byte for byte. Every field one of them writes and the other only reads is checked there and nowhere else.
- **`Tests/lint.sh`** checks SplitLint against a fixture written so that every line of it trips exactly one rule. It compares which warning came out and at which line rather than how many came out in total: a count stays right while the thing behind it goes wrong, and breaking one rule's message left the total untouched at twenty three.
- **`Tests/docs.sh`** checks the manuals against the code: that every instruction has a row and every row is an instruction, that the counts in the headings are right, that every directive is written down, that every service the system implements is described and every service described is implemented, that every routine the manuals promise exists, that CosmOS still fits in the half of the machine its memory map gives it, and that the worked examples still assemble to the bytes printed beside them.
A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out before anything is compared, keeping only whether the program stopped on its own or ran into its limit, which is behaviour. Two instructions added to CosmOS used to move that number in six unrelated files at once, so a real difference would have arrived in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own. A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out before anything is compared, keeping only whether the program stopped on its own or ran into its limit, which is behaviour. Two instructions added to CosmOS used to move that number in six unrelated files at once, so a real difference would have arrived in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own.
To rebuild all three tools with the address and undefined behaviour sanitizers and run the suite under them: To rebuild all four tools with the address and undefined behaviour sanitizers and run the suite under them:
``` ```
make sanitize make sanitize
+12 -1
View File
@@ -102,6 +102,7 @@ void printUsage(const char *programName) {
printf(" -o <file> Write the output to this path instead of alongside the source.\n"); printf(" -o <file> Write the output to this path instead of alongside the source.\n");
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n"); printf(" -I <dir> Look in this directory for included files. May be given more than once.\n");
printf(" -M <file> Write the source files this output depends on, as a make rule.\n"); printf(" -M <file> Write the source files this output depends on, as a make rule.\n");
printf(" -S <file> Write every label and the address it was given, in address order.\n");
printf(" -h, --help Display this help message.\n"); printf(" -h, --help Display this help message.\n");
} }
@@ -171,15 +172,17 @@ int main(int argc, char *argv[]) {
{"output", required_argument, 0, 'o'}, {"output", required_argument, 0, 'o'},
{"include", required_argument, 0, 'I'}, {"include", required_argument, 0, 'I'},
{"depend", required_argument, 0, 'M'}, {"depend", required_argument, 0, 'M'},
{"symbols", required_argument, 0, 'S'},
{"help", no_argument, 0, 'h'}, {"help", no_argument, 0, 'h'},
{0, 0, 0, 0 } {0, 0, 0, 0 }
}; };
char *outputFileName = NULL; char *outputFileName = NULL;
char *dependencyFileName = NULL; char *dependencyFileName = NULL;
char *symbolFileName = NULL;
int option_index = 0; int option_index = 0;
int opt; int opt;
while ((opt = getopt_long(argc, argv, "o:I:M:h", long_options, &option_index)) != -1) { while ((opt = getopt_long(argc, argv, "o:I:M:S:h", long_options, &option_index)) != -1) {
switch (opt) { switch (opt) {
case 'o': case 'o':
outputFileName = strdup(optarg); outputFileName = strdup(optarg);
@@ -190,6 +193,9 @@ int main(int argc, char *argv[]) {
case 'M': case 'M':
dependencyFileName = strdup(optarg); dependencyFileName = strdup(optarg);
break; break;
case 'S':
symbolFileName = strdup(optarg);
break;
case 'h': case 'h':
printUsage(argv[0]); printUsage(argv[0]);
return 0; return 0;
@@ -249,6 +255,11 @@ int main(int argc, char *argv[]) {
writeDependencyFile(dependencyFileName, outputFileName); writeDependencyFile(dependencyFileName, outputFileName);
free(dependencyFileName); free(dependencyFileName);
} }
// Before the cleanup, which is what frees the label table this reads.
if (symbolFileName) {
writeSymbolFile(symbolFileName);
free(symbolFileName);
}
assemblerCleanup(intermediateArray, index, outputFileName); assemblerCleanup(intermediateArray, index, outputFileName);
return 0; return 0;
} }
+3 -3
View File
@@ -122,11 +122,11 @@ int checkIfInstruction(intermediateElement *currentElement) {
dot = strchr(dot, '.'); dot = strchr(dot, '.');
} }
uint8_t opcode = getOpcode(token); int found = getOpcode(token);
if (opcode == 0xFE) { if (found == NOT_AN_OPCODE) {
// It's not an instruction.
return 0; return 0;
} }
uint8_t opcode = (uint8_t)found;
int selectorsWanted = dataPointerOperands(opcode); int selectorsWanted = dataPointerOperands(opcode);
if (selectorsGiven > selectorsWanted) { if (selectorsGiven > selectorsWanted) {
+60 -26
View File
@@ -12,34 +12,54 @@ typedef struct {
} Instruction; } Instruction;
Instruction instruction_set[] = { Instruction instruction_set[] = {
// ---- Nothing at all in 0x00 to 0x0F ----
//
// Kept empty on purpose. Program Memory that has never been written, or a load that
// stopped part way and left zeroes in its tail, used to read as a long run of
// additions and then do something unpredictable a long way from the cause. An
// unassigned byte faults where it is met, with the address, which is the difference
// between a diagnosis and a search.
//
// Arithmetic and Logic Operations: // Arithmetic and Logic Operations:
{0x00, "ADD"}, {0x10, "ADD"},
{0x01, "SUB"}, {0x11, "SUB"},
{0x02, "AND"}, {0x12, "AND"},
{0x03, "OR"}, {0x13, "OR"},
{0x04, "XOR"}, {0x14, "XOR"},
{0x05, "NOTA"}, {0x15, "NOTA"},
{0x06, "NOTB"}, {0x16, "NOTB"},
{0x07, "SHL"}, {0x17, "SHL"},
{0x08, "SHR"}, {0x18, "SHR"},
// Branch Operations: // Branch Operations:
{0x10, "BRI"}, {0x60, "BRI"},
{0x11, "BRQ"}, {0x61, "BRQ"},
{0x12, "BRA"}, {0x62, "BRA"},
{0x13, "BRB"}, {0x63, "BRB"},
{0x14, "BRC"}, {0x64, "BRC"},
{0x15, "BRD"}, {0x65, "BRD"},
// The same four conditions the other way round. A quarter of the conditional // The same four conditions the other way round. A quarter of the conditional
// branches in the corpus were a branch over an unconditional one before these // branches in the corpus were a branch over an unconditional one before these
// existed, each of them needing a label invented only to be jumped past. // existed, each of them needing a label invented only to be jumped past.
{0x1A, "BNQ"}, {0x66, "BNQ"},
{0x1B, "BNA"}, {0x67, "BNA"},
{0x1C, "BNB"}, {0x68, "BNB"},
{0x1D, "BNC"}, {0x69, "BNC"},
{0x17, "CALL"}, // Subroutine Operations:
{0x18, "SWI"}, //
{0x19, "RETI"}, // A block of their own since the branches and these outgrew one nibble between them.
{0x1F, "RET"}, // Each raw form sits immediately below the ordinary one it cannot be mixed with: RCAL
// under CALL, RRET under RET, because the frames differ and returning through the
// wrong one takes the machine somewhere nobody named.
{0x70, "RCAL"},
{0x71, "CALL"},
{0x72, "SWI"},
{0x73, "RETI"},
{0x74, "RRET"},
{0x75, "RET"},
// A handler with an answer. RETI restores everything and is how a hardware handler
// says it was never here; this restores what a RET restores, and is how a service
// says it has replied. See the note in cpu.c.
{0x76, "SRET"},
// Register Operations: // Register Operations:
{0x20, "RSTA"}, {0x20, "RSTA"},
{0x21, "RSTB"}, {0x21, "RSTB"},
@@ -77,6 +97,10 @@ Instruction instruction_set[] = {
{0x4B, "STD"}, {0x4B, "STD"},
{0x4C, "MVSD"}, {0x4C, "MVSD"},
{0x4D, "MVDS"}, {0x4D, "MVDS"},
{0x4E, "DPUA"},
{0x4F, "DPDA"},
{0x50, "DPUW"},
{0x51, "DPDW"},
// Output Operations: // Output Operations:
{0xD0, "OUTQ"}, {0xD0, "OUTQ"},
{0xD1, "OUTA"}, {0xD1, "OUTA"},
@@ -86,6 +110,7 @@ Instruction instruction_set[] = {
{0xE1, "INB"}, {0xE1, "INB"},
// Special Operations: // Special Operations:
{0xF0, "NOP"}, {0xF0, "NOP"},
{0xFE, "WAIT"},
{0xFF, "HALT"} {0xFF, "HALT"}
}; };
@@ -108,7 +133,7 @@ int dataPointerOperands(uint8_t opcode) {
case 0x4A: // LDD case 0x4A: // LDD
case 0x4B: // STD case 0x4B: // STD
return 2; return 2;
case 0x15: // BRD case 0x65: // BRD
case 0x33: // PSHD case 0x33: // PSHD
case 0x36: // POPD case 0x36: // POPD
case 0x40: // INCD case 0x40: // INCD
@@ -123,19 +148,28 @@ int dataPointerOperands(uint8_t opcode) {
case 0x49: // DPDN case 0x49: // DPDN
case 0x4C: // MVSD case 0x4C: // MVSD
case 0x4D: // MVDS case 0x4D: // MVDS
case 0x4E: // DPUA
case 0x4F: // DPDA
case 0x50: // DPUW
case 0x51: // DPDW
return 1; return 1;
default: default:
return 0; return 0;
} }
} }
uint8_t getOpcode(char* mnemonic) { int getOpcode(char* mnemonic) {
for (int i = 0; i < num_instructions; i++) { for (int i = 0; i < num_instructions; i++) {
if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) { if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) {
return instruction_set[i].opcode; return instruction_set[i].opcode;
} }
} }
return 0xFE; // FE is an unused instruction, we'll use it to indicate an error. // NOT_AN_OPCODE, and it is negative on purpose. This used to answer 0xFE on the
// grounds that 0xFE was unused - which was true until WAIT was given that opcode, at
// which point the assembler would have read WAIT as a word it did not recognise. A
// sentinel picked from the unused half of a range stops being a sentinel the moment
// somebody uses the range, so this one is outside the range altogether.
return NOT_AN_OPCODE;
} }
+5 -1
View File
@@ -119,7 +119,11 @@
const char* getMnemonic(uint8_t opcode); const char* getMnemonic(uint8_t opcode);
uint8_t getOpcode(char* mnemonic); // The opcode a mnemonic assembles to, or NOT_AN_OPCODE if the word is not one. The
// return is an int rather than a byte so that the answer "no" cannot be confused with any
// of the 256 answers "yes" - see the note in getOpcode.
#define NOT_AN_OPCODE (-1)
int getOpcode(char* mnemonic);
// How many Data Pointer selector bytes follow the given opcode. Never more than two. // How many Data Pointer selector bytes follow the given opcode. Never more than two.
#define MAX_DATA_POINTER_OPERANDS 2 #define MAX_DATA_POINTER_OPERANDS 2
+1 -1
View File
@@ -385,7 +385,7 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
&& (*intermediateArray)[*intermediateIndex].type == LABEL && (*intermediateArray)[*intermediateIndex].type == LABEL
&& *intermediateIndex > 0 && *intermediateIndex > 0
&& (*intermediateArray)[*intermediateIndex - 1].type == INSTRUCTION && (*intermediateArray)[*intermediateIndex - 1].type == INSTRUCTION
&& (*intermediateArray)[*intermediateIndex - 1].byteValue == 0x18) { && (*intermediateArray)[*intermediateIndex - 1].byteValue == 0x72) {
(*intermediateArray)[*intermediateIndex].type = VECTOR_REFERENCE; (*intermediateArray)[*intermediateIndex].type = VECTOR_REFERENCE;
(*intermediateArray)[*intermediateIndex].byteLength = 1; (*intermediateArray)[*intermediateIndex].byteLength = 1;
} }
+76 -6
View File
@@ -21,6 +21,48 @@ int debugSecondPass = 0;
Label labelArray[MAX_LABELS]; Label labelArray[MAX_LABELS];
int labelCount = 0; int labelCount = 0;
// ---- Where everything ended up ----
//
// Every label and the address it was given, in address order. The assembler knows this and
// nothing else does: a program on the disk is bytes, and the machine's own monitor can
// disassemble it but has no idea what any of it is called.
//
// WHAT IT IS FOR is telling where a program spends its time. Counting which addresses get
// called says a great deal and names nothing, so the answer arrives as a list of numbers
// and somebody has to work out by hand which routine each one is inside. With this, a
// tally of call targets becomes a list of routine names.
//
// Sorted by address rather than by name, because the question asked of it is always "what
// is at this address", and a label table is small enough that sorting it is free.
static int byAddress(const void *left, const void *right) {
const Label *a = left, *b = right;
if (a->address != b->address) {
return a->address < b->address ? -1 : 1;
}
return strcmp(a->label, b->label);
}
void writeSymbolFile(const char *path) {
FILE *file = fopen(path, "w");
if (!file) {
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, path);
exit(1);
}
Label *sorted = malloc((size_t)labelCount * sizeof(Label));
if (!sorted) {
fprintf(stderr, RED "Error: Out of memory writing the symbol file.\n" RESET);
fclose(file);
exit(1);
}
memcpy(sorted, labelArray, (size_t)labelCount * sizeof(Label));
qsort(sorted, (size_t)labelCount, sizeof(Label), byAddress);
for (int i = 0; i < labelCount; i++) {
fprintf(file, "%04X %s\n", sorted[i].address, sorted[i].label);
}
free(sorted);
fclose(file);
}
void freeLabelList() { void freeLabelList() {
for (int i = 0; i < labelCount; i++) { for (int i = 0; i < labelCount; i++) {
if (labelArray[i].label) { if (labelArray[i].label) {
@@ -39,6 +81,34 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
cleanedLabel[len - 1] = '\0'; // Remove the colon cleanedLabel[len - 1] = '\0'; // Remove the colon
} }
// A NAME THAT IS ALREADY AN INSTRUCTION IS REFUSED, and the error is here rather
// than at the branch that could not find it. Mnemonics are matched with the token
// uppercased, so a label called "wait" and the instruction WAIT are the same word
// - and when WAIT was added, Keys.asm had been using that label for a year. What
// it reported was "Branch without label" at the BRQ, thirty lines from the cause
// and naming nothing that had changed.
//
// This will happen again. Every instruction added takes a word out of the space
// of label names, so the check belongs where the name is claimed.
{
char upper[8];
int n = 0;
for (; cleanedLabel[n] != '\0' && n < (int)sizeof(upper) - 1; n++) {
upper[n] = (char)toupper((unsigned char)cleanedLabel[n]);
}
upper[n] = '\0';
// Only a name short enough to BE a mnemonic can collide with one, and the
// longest is four characters. A longer name is truncated by the loop above
// and would not match anything, which is the right answer.
if (cleanedLabel[n] == '\0' && getOpcode(upper) != NOT_AN_OPCODE) {
fprintf(stderr, RED "Error: \"%s\" is an instruction, so it cannot also be"
" a label.\n" RESET, cleanedLabel);
printf("File: %s at line %d.\n", fileName, lineNumber);
free(cleanedLabel);
exit(1);
}
}
// A name may only be defined once. Without this check a reference quietly // A name may only be defined once. Without this check a reference quietly
// resolves to whichever definition came first, so a typo or a name that two // resolves to whichever definition came first, so a typo or a name that two
// libraries both happen to use is very hard to track down. // libraries both happen to use is very hard to track down.
@@ -442,17 +512,17 @@ static void checkOperands(intermediateElement *intermediateArray, int arraySize,
const char *problem = NULL; const char *problem = NULL;
// Listed rather than matched on the high nibble, because not every instruction in // Listed rather than matched on the high nibble, because not every instruction in
// the branch block takes an address: RET has none, and BRD gets its destination // these two blocks takes an address: RET has none, and BRD gets its destination from
// from a Data Pointer instead of from the program. // a Data Pointer instead of from the program.
if (opcode == 0x10 || opcode == 0x11 || opcode == 0x12 || if (opcode == 0x60 || opcode == 0x61 || opcode == 0x62 ||
opcode == 0x13 || opcode == 0x14 || opcode == 0x17 || opcode == 0x63 || opcode == 0x64 || opcode == 0x71 ||
opcode == 0x1A || opcode == 0x1B || opcode == 0x1C || opcode == 0x1D) { opcode == 0x66 || opcode == 0x67 || opcode == 0x68 || opcode == 0x69) {
// Branches and CALL take a two byte address, which only a label can supply. // Branches and CALL take a two byte address, which only a label can supply.
if (nextType != LABEL) problem = "Branch without label."; if (nextType != LABEL) problem = "Branch without label.";
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) { } else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
// The instruction is either an input or output and must be followed by a value. // The instruction is either an input or output and must be followed by a value.
if (nextType != VALUE) problem = "I/O without destination port."; if (nextType != VALUE) problem = "I/O without destination port.";
} else if (opcode == 0x18) { } else if (opcode == 0x72) {
// SWI names a vector, either by the name it was given in the Vector Segment or, // SWI names a vector, either by the name it was given in the Vector Segment or,
// rarely, as a literal number. Without one it swallows whatever follows it and // rarely, as a literal number. Without one it swallows whatever follows it and
// every address after that shifts. // every address after that shifts.
+4
View File
@@ -24,6 +24,10 @@ typedef struct {
int type; int type;
} Label; } Label;
// Every label and the address it was given, in address order, so that a tally of
// addresses can be turned back into a list of routine names.
void writeSymbolFile(const char *path);
// One line of the Vector Segment, once it has been worked out. // One line of the Vector Segment, once it has been worked out.
typedef struct { typedef struct {
char* name; // What it was called, or NULL for a device, which is named by its port. char* name; // What it was called, or NULL for a device, which is named by its port.
+766 -52
View File
@@ -51,10 +51,14 @@ static int writeBlock(FILE *image, uint16_t block, const uint8_t *from) {
} }
typedef struct { typedef struct {
uint8_t version;
uint16_t diskBlocks; uint16_t diskBlocks;
uint16_t directoryStart; uint16_t directoryStart;
uint16_t directoryBlocks; uint16_t directoryBlocks;
uint16_t freeBlocks; uint16_t freeBlocks;
uint16_t bootBlocks; // Per slot. Zero on a disk that cannot be booted.
uint8_t bootSlot; // Which of the two is live.
uint8_t bootState; // How the last start went. See sbfs.h.
} Superblock; } Superblock;
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and // Reads block 0 and checks it really is one of ours. Without the magic a blank image and
@@ -68,15 +72,49 @@ static int readSuperblock(FILE *image, Superblock *super) {
fprintf(stderr, "Error: That is not a SplitBit disk. Format it first.\n"); fprintf(stderr, "Error: That is not a SplitBit disk. Format it first.\n");
return 1; return 1;
} }
if (block[SBFS_SUPER_VERSION] != SBFS_VERSION) { // Either version is read. A version one disk has zeroes in the bytes version two
fprintf(stderr, "Error: That disk is version %u, and this understands version %u.\n", // uses for a parent, and zero is the root, so every file on it reads as a file in the
block[SBFS_SUPER_VERSION], SBFS_VERSION); // root - which is exactly where it is. Nothing is converted here.
if (block[SBFS_SUPER_VERSION] != SBFS_VERSION_FLAT
&& block[SBFS_SUPER_VERSION] != SBFS_VERSION_TREE) {
fprintf(stderr, "Error: That disk is version %u, and this understands %u and %u.\n",
block[SBFS_SUPER_VERSION], SBFS_VERSION_FLAT, SBFS_VERSION_TREE);
return 1; return 1;
} }
// A directory big enough that its last entries cannot be named as a parent. See
// sbfs.h: those entries do not refuse what is put in them, they quietly put it in the
// root instead. Refused on the way in, so that nothing below ever has to wonder.
uint16_t directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
fprintf(stderr, "Error: That disk claims %u directory blocks, and %u is the most"
" that leaves every entry able to be named as a parent.\n",
directoryBlocks, SBFS_MAX_DIRECTORY_BLOCKS);
return 1;
}
super->version = block[SBFS_SUPER_VERSION];
super->diskBlocks = readWord(block + SBFS_SUPER_DISK); super->diskBlocks = readWord(block + SBFS_SUPER_DISK);
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART); super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
super->directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS); super->directoryBlocks = directoryBlocks;
super->freeBlocks = readWord(block + SBFS_SUPER_FREE); super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
super->bootBlocks = readWord(block + SBFS_SUPER_BOOTBLOCKS);
super->bootSlot = block[SBFS_SUPER_BOOTSLOT];
super->bootState = block[SBFS_SUPER_BOOTSTATE];
// The boot area and the directory's position describe the same fact from two sides,
// so they have to agree or one of them is wrong and there is no way to tell which.
uint32_t expected = SBFS_FIRST_BOOT_BLOCK
+ (uint32_t)super->bootBlocks * SBFS_BOOT_SLOTS;
if (super->directoryStart != expected) {
fprintf(stderr, "Error: That disk says %u blocks of boot area and puts its"
" directory at %u, which should then be %u.\n",
super->bootBlocks, super->directoryStart, expected);
return 1;
}
if (super->bootSlot >= SBFS_BOOT_SLOTS) {
fprintf(stderr, "Error: That disk names boot slot %u, and there are %u.\n",
super->bootSlot, SBFS_BOOT_SLOTS);
return 1;
}
return 0; return 0;
} }
@@ -84,11 +122,17 @@ static int writeSuperblock(FILE *image, const Superblock *super) {
uint8_t block[SBFS_BLOCK_BYTES]; uint8_t block[SBFS_BLOCK_BYTES];
memset(block, 0, sizeof(block)); memset(block, 0, sizeof(block));
memcpy(block, SBFS_MAGIC, SBFS_MAGIC_BYTES); memcpy(block, SBFS_MAGIC, SBFS_MAGIC_BYTES);
block[SBFS_SUPER_VERSION] = SBFS_VERSION; // Written back at the version it was read at. Putting a file on a flat disk leaves it
// flat; only creating a directory on it makes the difference real, and only mkdir
// raises the number.
block[SBFS_SUPER_VERSION] = super->version;
writeWord(block + SBFS_SUPER_DISK, super->diskBlocks); writeWord(block + SBFS_SUPER_DISK, super->diskBlocks);
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart); writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks); writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks); writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
writeWord(block + SBFS_SUPER_BOOTBLOCKS, super->bootBlocks);
block[SBFS_SUPER_BOOTSLOT] = super->bootSlot;
block[SBFS_SUPER_BOOTSTATE] = super->bootState;
return writeBlock(image, 0, block); return writeBlock(image, 0, block);
} }
@@ -136,6 +180,28 @@ static int entryInUse(const uint8_t *entry) {
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_IN_USE) != 0; return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_IN_USE) != 0;
} }
static int entryIsDirectory(const uint8_t *entry) {
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_DIRECTORY) != 0;
}
// A save that was interrupted between writing its temporary and committing it. The blocks
// are genuinely spoken for - entryInUse says so, and the allocator must keep believing it
// - but nothing has claimed them under a name anybody asked for.
static int entryIsTemporary(const uint8_t *entry) {
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_TEMPORARY) != 0;
}
// The index of the entry this one lives in, or -1 for the root. Stored as index plus one
// so that a zeroed field - which is what every version one entry has - means the root.
static int entryParent(const uint8_t *entry) {
return SBFS_PARENT_INDEX(readWord(entry + SBFS_ENTRY_PARENT));
}
static void entrySetParent(uint8_t *entry, int parent) {
writeWord(entry + SBFS_ENTRY_PARENT,
parent < 0 ? SBFS_PARENT_ROOT : SBFS_PARENT_OF(parent));
}
static uint32_t entrySize(const uint8_t *entry) { static uint32_t entrySize(const uint8_t *entry) {
return (uint32_t)readWord(entry + SBFS_ENTRY_BLOCKS) * SBFS_BLOCK_BYTES return (uint32_t)readWord(entry + SBFS_ENTRY_BLOCKS) * SBFS_BLOCK_BYTES
+ entry[SBFS_ENTRY_TAIL]; + entry[SBFS_ENTRY_TAIL];
@@ -153,13 +219,58 @@ static void entryName(const uint8_t *entry, char *into) {
into[SBFS_NAME_BYTES] = '\0'; into[SBFS_NAME_BYTES] = '\0';
} }
static int findByName(const Directory *directory, const char *name) { // ---- Paths ----
//
// A path is names separated by '/', and every path this tool is given is from the root:
// there is no working directory on the host, and one would have nowhere to live between
// two runs of a command line tool. The machine is the side that gets one of those.
//
// The root is not an entry. It is the absence of a parent, so -1 stands for it throughout
// and is a perfectly good answer rather than a failure - which is why every function here
// returns its outcome separately from the index it found.
// How long a path this tool will carry. Nothing in the format says: a path is not stored
// anywhere, it is only ever walked, and what is stored is one name and one parent.
#define SBFS_PATH_BYTES 512
// Copies the next name out of a path and returns where the path goes on, or NULL when
// there are no more. Empty pieces - a leading separator, a doubled one, a trailing one -
// are skipped rather than refused, so "/Apps/" and "Apps" walk the same way.
static const char *nextComponent(const char *path, char *into, int *tooLong) {
while (*path == SBFS_SEPARATOR) {
path++;
}
if (*path == '\0') {
return NULL;
}
size_t n = 0;
while (*path != '\0' && *path != SBFS_SEPARATOR) {
if (n < SBFS_NAME_BYTES) {
into[n] = *path;
}
n++;
path++;
}
*tooLong = (n > SBFS_NAME_BYTES);
into[n > SBFS_NAME_BYTES ? SBFS_NAME_BYTES : n] = '\0';
return path;
}
// One named child of one directory. `parent` is an entry index, or -1 for the root.
//
// This is the whole of what a directory is. There is no list of children anywhere: being
// a child is a fact written in the child, so finding them means looking at all of them.
// That is the same walk the flat version did, with one more thing compared.
static int findIn(const Directory *directory, const char *name, int parent) {
char held[SBFS_NAME_BYTES + 1]; char held[SBFS_NAME_BYTES + 1];
for (int i = 0; i < directory->entries; i++) { for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i); const uint8_t *entry = entryAt(directory, i);
if (!entryInUse(entry)) { if (!entryInUse(entry)) {
continue; continue;
} }
if (entryParent(entry) != parent) {
continue;
}
entryName(entry, held); entryName(entry, held);
if (strcmp(held, name) == 0) { if (strcmp(held, name) == 0) {
return i; return i;
@@ -168,6 +279,102 @@ static int findByName(const Directory *directory, const char *name) {
return -1; return -1;
} }
// Walks a whole path. Zero if it got there, and then *index is the entry it names or -1
// for the root. Anything else leaves *why saying what stopped it.
static int resolve(const Directory *directory, const char *path, int *index,
const char **why) {
int at = -1;
char name[SBFS_NAME_BYTES + 1];
int tooLong = 0;
const char *rest = path;
while ((rest = nextComponent(rest, name, &tooLong)) != NULL) {
if (tooLong) {
*why = "has a name longer than 22 characters in it";
return 1;
}
if (strcmp(name, ".") == 0) {
continue;
}
if (strcmp(name, "..") == 0) {
if (at >= 0) {
at = entryParent(entryAt(directory, at));
}
continue; // ".." from the root is the root.
}
// Only a directory can be walked through. The LAST thing on the path is not
// checked here, because only the caller knows whether it wanted a file or a
// directory, and it can say so far better than this can.
if (at >= 0 && !entryIsDirectory(entryAt(directory, at))) {
*why = "has something in it that is not a directory";
return 1;
}
int found = findIn(directory, name, at);
if (found < 0) {
*why = "is not there";
return 1;
}
at = found;
}
*index = at;
return 0;
}
// Walks all but the last name, so that a caller can make the last one. *parent comes back
// as the directory to make it in, and `leaf` as the name to make. `leaf` must have room
// for SBFS_NAME_BYTES + 1.
static int resolveParent(const Directory *directory, const char *path, char *leaf,
int *parent, const char **why) {
const char *cut = strrchr(path, SBFS_SEPARATOR);
const char *last = cut ? cut + 1 : path;
if (*last == '\0') {
*why = "ends in a separator, so it does not name anything";
return 1;
}
if (strlen(last) > SBFS_NAME_BYTES) {
*why = "ends in a name longer than 22 characters";
return 1;
}
if (strcmp(last, ".") == 0 || strcmp(last, "..") == 0) {
*why = "ends in a name that is already taken by the filesystem";
return 1;
}
strcpy(leaf, last);
if (cut == NULL) {
*parent = -1; // A bare name belongs in the root.
return 0;
}
char head[SBFS_PATH_BYTES];
size_t n = (size_t)(cut - path);
if (n >= sizeof(head)) {
*why = "is longer than this tool will carry";
return 1;
}
memcpy(head, path, n);
head[n] = '\0';
return resolve(directory, head, parent, why); // "" is the root, which is correct.
}
// Whether something can be made inside *parent at all. The root always can.
static int parentIsUsable(const Directory *directory, int parent, const char *path) {
if (parent >= 0 && !entryIsDirectory(entryAt(directory, parent))) {
fprintf(stderr, "Error: \"%s\" is inside something that is not a directory.\n", path);
return 0;
}
return 1;
}
// Whether a directory has anything in it. Nothing points down, so this is the only way
// to know: something is in it if something says it is.
static int directoryHasChildren(const Directory *directory, int parent) {
for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i);
if (entryInUse(entry) && entryParent(entry) == parent) {
return 1;
}
}
return 0;
}
// Where the first free run of the wanted length begins, or -1 if there is not one. // Where the first free run of the wanted length begins, or -1 if there is not one.
// //
// There is no allocation bitmap, and that is the design rather than an omission: with // There is no allocation bitmap, and that is the design rather than an omission: with
@@ -224,10 +431,22 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
// ---- Commands ---- // ---- Commands ----
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) { static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks,
if (blocks <= 1u + directoryBlocks) { uint16_t bootBlocks) {
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock and a" if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
" directory of %u.\n", blocks, directoryBlocks); fprintf(stderr, "Error: %u directory blocks is %u entries, and entry 65535 has no"
" parent number - adding one wraps to zero, which is the root."
" %u blocks is the most, giving %u entries.\n",
directoryBlocks, (unsigned)directoryBlocks * SBFS_ENTRIES_PER_BLOCK,
SBFS_MAX_DIRECTORY_BLOCKS, SBFS_MAX_ENTRIES);
return 1;
}
uint32_t overhead = 1u + (uint32_t)bootBlocks * SBFS_BOOT_SLOTS + directoryBlocks;
if (blocks <= overhead) {
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock, %u of"
" boot area and a directory of %u.\n",
blocks, (unsigned)((uint32_t)bootBlocks * SBFS_BOOT_SLOTS),
directoryBlocks);
return 1; return 1;
} }
// Quietly, because a disk that is not there yet is the ordinary case for format and // Quietly, because a disk that is not there yet is the ordinary case for format and
@@ -248,21 +467,116 @@ static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBl
} }
} }
Superblock super; Superblock super;
// A DISK IS AT THE LOWEST VERSION THAT DESCRIBES WHAT IS ON IT, and a fresh one has
// no directories on it, so it is flat and flat is version one. The number says what
// the disk contains, not which tool made it - which is what lets everything built
// here still be mounted by a reader that has never heard of a directory. mkdir is
// what raises it, because mkdir is what makes the difference true.
super.version = SBFS_VERSION_FLAT;
super.diskBlocks = blocks; super.diskBlocks = blocks;
super.directoryStart = SBFS_FIRST_DIRECTORY_BLOCK; // THE DIRECTORY MOVES UP BY THE BOOT AREA, and that is the whole mechanism. Both
// implementations already work out the first usable block as directoryStart plus
// directoryBlocks, so everything below the directory is reserved by arithmetic that
// was there before any of this, and no allocator changed.
super.directoryStart = (uint16_t)(SBFS_FIRST_BOOT_BLOCK
+ (uint32_t)bootBlocks * SBFS_BOOT_SLOTS);
super.directoryBlocks = directoryBlocks; super.directoryBlocks = directoryBlocks;
super.freeBlocks = (uint16_t)(blocks - 1 - directoryBlocks); super.freeBlocks = (uint16_t)(blocks - overhead);
super.bootBlocks = bootBlocks;
super.bootSlot = 0;
super.bootState = SBFS_BOOT_SETTLED;
if (writeSuperblock(image, &super)) { if (writeSuperblock(image, &super)) {
fclose(image); fclose(image);
return 1; return 1;
} }
fclose(image); fclose(image);
if (bootBlocks) {
printf("Formatted %s: %u blocks, two boot slots of %u, %u of directory, %u free.\n",
path, blocks, bootBlocks, directoryBlocks, super.freeBlocks);
return 0;
}
printf("Formatted %s: %u blocks, %u of directory, %u free.\n", printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
path, blocks, directoryBlocks, super.freeBlocks); path, blocks, directoryBlocks, super.freeBlocks);
return 0; return 0;
} }
static int commandList(const char *path) { // The full path of an entry, built by walking up the parents and writing them out
// backwards. Nothing stores a path, so this is the only way to have one.
//
// The depth cap is what makes this safe on a disk whose parents form a loop: walking up
// would otherwise never reach the root. Running out of depth is reported as not fitting,
// which is what it is.
static int entryPath(const Directory *directory, int index, char *into, size_t room) {
int chain[64];
int depth = 0;
while (index >= 0 && depth < (int)(sizeof(chain) / sizeof(chain[0]))) {
chain[depth++] = index;
index = entryParent(entryAt(directory, index));
}
if (index >= 0) {
return -1; // Deeper than this will walk, or a loop.
}
size_t at = 0;
char name[SBFS_NAME_BYTES + 1];
for (int i = depth - 1; i >= 0; i--) {
entryName(entryAt(directory, chain[i]), name);
size_t n = strlen(name);
if (at + n + 2 > room) {
return -1;
}
into[at++] = SBFS_SEPARATOR;
memcpy(into + at, name, n);
at += n;
}
into[at] = '\0';
return (int)at;
}
// What a listing added up to. Kept together because the two numbers are only meaningful
// beside each other: a disk runs out of entries, not of files.
typedef struct {
int files;
int directories;
int temporaries;
} Tally;
// Prints one directory and everything under it, depth first and in entry order, which is
// the order the machine will walk them in too.
//
// Recursing from the root can never loop, because it only ever descends into entries
// whose parent is where it already is. A cycle among entries is therefore not an infinite
// walk - it is a set of entries this never reaches, which is what the caller counts.
static void listTree(const Directory *directory, int parent, char *prefix, size_t at,
Tally *tally) {
char name[SBFS_NAME_BYTES + 1];
for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i);
if (!entryInUse(entry) || entryParent(entry) != parent) {
continue;
}
entryName(entry, name);
size_t n = strlen(name);
if (at + n + 2 >= SBFS_PATH_BYTES) {
continue; // Deeper than this tool will print.
}
prefix[at] = SBFS_SEPARATOR;
memcpy(prefix + at + 1, name, n + 1);
if (entryIsDirectory(entry)) {
printf("%-34s %8s %7s %7s\n", prefix, "dir", "-", "-");
tally->directories++;
listTree(directory, i, prefix, at + 1 + n, tally);
} else {
printf("%-34s %8u %7u %7u%s\n", prefix, entrySize(entry),
readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry),
entryIsTemporary(entry) ? " unfinished" : "");
tally->files++;
tally->temporaries += entryIsTemporary(entry);
}
prefix[at] = '\0';
}
}
static int commandList(const char *path, const char *within) {
FILE *image = openImage(path, "rb"); FILE *image = openImage(path, "rb");
if (image == NULL) { if (image == NULL) {
return 1; return 1;
@@ -273,34 +587,250 @@ static int commandList(const char *path) {
fclose(image); fclose(image);
return 1; return 1;
} }
printf("%s: %u blocks, %u of directory, %u entries.\n", printf("%s: %u blocks, %u of directory, %u entries, version %u.\n",
path, super.diskBlocks, super.directoryBlocks, directory.entries); path, super.diskBlocks, super.directoryBlocks, directory.entries,
printf("%-22s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS"); super.version);
char name[SBFS_NAME_BYTES + 1]; printf("%-34s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS");
int shown = 0; char prefix[SBFS_PATH_BYTES];
for (int i = 0; i < directory.entries; i++) { Tally tally = { 0, 0, 0 };
const uint8_t *entry = entryAt(&directory, i); int start = -1;
if (!entryInUse(entry)) { if (within != NULL) {
continue; const char *why = NULL;
if (resolve(&directory, within, &start, &why)) {
fprintf(stderr, "Error: \"%s\" %s.\n", within, why);
free(directory.bytes);
fclose(image);
return 1;
}
if (start >= 0 && !entryIsDirectory(entryAt(&directory, start))) {
fprintf(stderr, "Error: \"%s\" is not a directory.\n", within);
free(directory.bytes);
fclose(image);
return 1;
} }
entryName(entry, name);
printf("%-22s %8u %7u %7u\n", name, entrySize(entry),
readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry));
shown++;
} }
// Listing one directory still prints whole paths, because a path that is only true
// relative to an argument the reader cannot see is worse than no path at all.
size_t from = 0;
prefix[0] = '\0';
if (start >= 0) {
int n = entryPath(&directory, start, prefix, sizeof(prefix));
if (n < 0) {
fprintf(stderr, "Error: \"%s\" is nested too deep to print.\n", within);
free(directory.bytes);
fclose(image);
return 1;
}
from = (size_t)n;
}
listTree(&directory, start, prefix, from, &tally);
// The count in the superblock is a cache, so say what the directory actually adds up // The count in the superblock is a cache, so say what the directory actually adds up
// to as well. If the two ever disagree, the directory is the one to believe. // to as well. If the two ever disagree, the directory is the one to believe.
uint16_t counted = countFree(&directory, &super); uint16_t counted = countFree(&directory, &super);
printf("%d file%s, %u blocks free", shown, shown == 1 ? "" : "s", counted); printf("%d file%s", tally.files, tally.files == 1 ? "" : "s");
if (tally.directories > 0) {
printf(", %d director%s", tally.directories,
tally.directories == 1 ? "y" : "ies");
}
// Entries are the ceiling nobody notices until they hit it, and on a disk of small
// files they run out long before the blocks do. Saying both means never having to
// work out which one is about to bite.
printf(", %d of %d entries used, %u blocks free",
tally.files + tally.directories, directory.entries, counted);
if (counted != super.freeBlocks) { if (counted != super.freeBlocks) {
printf(" (the superblock says %u, which is stale)", super.freeBlocks); printf(" (the superblock says %u, which is stale)", super.freeBlocks);
} }
printf(".\n"); printf(".\n");
// A save that stopped between deleting the old entry and naming the new one. The
// bytes are all there under the temporary's name and one rename brings them back,
// which is the whole of the recovery this format offers - so the thing that matters
// is that a listing says so rather than showing a file with an odd name.
if (tally.temporaries > 0) {
printf("%d unfinished write%s: the blocks are held and the data is there, under"
" that name. Rename it to keep it, delete it to let the blocks go.\n",
tally.temporaries, tally.temporaries == 1 ? "" : "s");
}
// Everything in use should have been reached by walking down from the root. Anything
// that was not is pointing at a parent that is not there, or at itself, and that is
// worth saying out loud rather than quietly leaving off the listing.
if (within == NULL) {
int inUse = 0;
for (int i = 0; i < directory.entries; i++) {
if (entryInUse(entryAt(&directory, i))) {
inUse++;
}
}
if (inUse != tally.files + tally.directories) {
printf("%d entr%s in use but not reachable from the root.\n",
inUse - (tally.files + tally.directories),
inUse - (tally.files + tally.directories) == 1 ? "y is" : "ies are");
}
}
free(directory.bytes); free(directory.bytes);
fclose(image); fclose(image);
return 0; return 0;
} }
// ---- Writing a boot slot ----
//
// Raw blocks, outside the filesystem, with no entry and no name. That is what makes this
// different from put: there is nothing to rename, so the safety comes from writing the
// slot that is NOT live and moving one byte afterwards.
//
// The one byte is moved by a separate command on purpose. Writing a slot and choosing it
// are different decisions - a slot can be written now and chosen after it has been looked
// at - and putting them in one command would make every write a commitment.
static int commandBoot(const char *path, const char *hostFile, long slot) {
FILE *image = openImage(path, "r+b");
if (image == NULL) {
return 1;
}
Superblock super;
if (readSuperblock(image, &super)) {
fclose(image);
return 1;
}
if (super.bootBlocks == 0) {
fprintf(stderr, "Error: That disk has no boot area. Format it with one.\n");
fclose(image);
return 1;
}
if (slot < 0 || slot >= SBFS_BOOT_SLOTS) {
fprintf(stderr, "Error: There are %d boot slots, numbered 0 and %d.\n",
SBFS_BOOT_SLOTS, SBFS_BOOT_SLOTS - 1);
fclose(image);
return 1;
}
FILE *source = fopen(hostFile, "rb");
if (source == NULL) {
fprintf(stderr, "Error: Couldn't open \"%s\".\n", hostFile);
fclose(image);
return 1;
}
if (fseek(source, 0, SEEK_END) != 0) {
fprintf(stderr, "Error: Couldn't measure \"%s\".\n", hostFile);
fclose(source); fclose(image);
return 1;
}
long size = ftell(source);
rewind(source);
long blocks = (size + SBFS_BLOCK_BYTES - 1) / SBFS_BLOCK_BYTES;
if (blocks > super.bootBlocks) {
fprintf(stderr, "Error: \"%s\" is %ld bytes, which is %ld blocks, and a slot"
" holds %u.\n", hostFile, size, blocks, super.bootBlocks);
fclose(source); fclose(image);
return 1;
}
// THE WHOLE SLOT IS WRITTEN, not just the part the file fills. A slot holding the tail
// of whatever was there before is a slot whose contents depend on its history, and the
// first stage reads all of it without knowing where the file stopped.
uint16_t first = (uint16_t)(SBFS_FIRST_BOOT_BLOCK
+ (uint32_t)slot * super.bootBlocks);
uint8_t block[SBFS_BLOCK_BYTES];
for (uint16_t i = 0; i < super.bootBlocks; i++) {
memset(block, 0, sizeof(block));
size_t got = fread(block, 1, SBFS_BLOCK_BYTES, source);
if (got == 0 && ferror(source)) {
fprintf(stderr, "Error: Couldn't read \"%s\".\n", hostFile);
fclose(source); fclose(image);
return 1;
}
if (writeBlock(image, (uint16_t)(first + i), block)) {
fclose(source); fclose(image);
return 1;
}
}
fclose(source);
fclose(image);
printf("Wrote %s into boot slot %ld: %ld bytes in %u blocks from block %u.%s\n",
hostFile, slot, size, super.bootBlocks, first,
slot == super.bootSlot ? "" : " It is not the live slot.");
return 0;
}
// Choosing which slot the machine starts from. One byte, written on its own, so that the
// change from one system to another is a single block write that either happened or did
// not.
static int commandBootSlot(const char *path, long slot) {
FILE *image = openImage(path, "r+b");
if (image == NULL) {
return 1;
}
Superblock super;
if (readSuperblock(image, &super)) {
fclose(image);
return 1;
}
if (super.bootBlocks == 0) {
fprintf(stderr, "Error: That disk has no boot area.\n");
fclose(image);
return 1;
}
if (slot < 0 || slot >= SBFS_BOOT_SLOTS) {
fprintf(stderr, "Error: There are %d boot slots, numbered 0 and %d.\n",
SBFS_BOOT_SLOTS, SBFS_BOOT_SLOTS - 1);
fclose(image);
return 1;
}
super.bootSlot = (uint8_t)slot;
if (writeSuperblock(image, &super)) {
fclose(image);
return 1;
}
fclose(image);
printf("The machine now starts from boot slot %ld.\n", slot);
return 0;
}
// ---- How the last start went, from the host ----
//
// Shown with no argument and set with one. Setting it is how a disk that fell back is told
// to try again, which is a decision rather than a repair: the thing that did not start has
// to be fixed first, or the next start marks it and falls back once more.
static const char *bootStateName(uint8_t state) {
switch (state) {
case SBFS_BOOT_SETTLED: return "settled, so the next start will use the configuration";
case SBFS_BOOT_TRYING: return "trying, so the last start never arrived";
case SBFS_BOOT_FELLBACK: return "fell back, and will keep doing so until settled";
default: return "a number this does not recognise";
}
}
static int commandBootState(const char *path, const char *setting) {
FILE *image = openImage(path, setting ? "r+b" : "rb");
if (image == NULL) {
return 1;
}
Superblock super;
if (readSuperblock(image, &super)) {
fclose(image);
return 1;
}
if (setting == NULL) {
printf("%u: %s\n", super.bootState, bootStateName(super.bootState));
fclose(image);
return 0;
}
long wanted = strtol(setting, NULL, 0);
if (wanted < 0 || wanted > SBFS_BOOT_FELLBACK) {
fprintf(stderr, "Error: The boot state is 0, 1 or 2.\n");
fclose(image);
return 1;
}
super.bootState = (uint8_t)wanted;
if (writeSuperblock(image, &super)) {
fclose(image);
return 1;
}
fclose(image);
printf("%ld: %s\n", wanted, bootStateName((uint8_t)wanted));
return 0;
}
static int commandPut(const char *path, const char *hostFile, const char *asName) { static int commandPut(const char *path, const char *hostFile, const char *asName) {
FILE *source = fopen(hostFile, "rb"); FILE *source = fopen(hostFile, "rb");
if (source == NULL) { if (source == NULL) {
@@ -316,14 +846,6 @@ static int commandPut(const char *path, const char *hostFile, const char *asName
return 1; return 1;
} }
if (strlen(asName) > SBFS_NAME_BYTES) {
fprintf(stderr, "Error: \"%s\" is %zu characters, and a name may be %d.\n"
" Give a shorter one as the last argument.\n",
asName, strlen(asName), SBFS_NAME_BYTES);
fclose(source);
return 1;
}
FILE *image = openImage(path, "r+b"); FILE *image = openImage(path, "r+b");
if (image == NULL) { if (image == NULL) {
fclose(source); fclose(source);
@@ -336,8 +858,20 @@ static int commandPut(const char *path, const char *hostFile, const char *asName
fclose(image); fclose(image);
return 1; return 1;
} }
if (findByName(&directory, asName) >= 0) { // Where it goes and what it is called are one argument. A bare name still means the
fprintf(stderr, "Error: \"%s\" is already on the disk. Delete it first.\n", asName); // root, so every command line that worked before this still works.
char leaf[SBFS_NAME_BYTES + 1];
int parent = -1;
const char *why = NULL;
if (resolveParent(&directory, asName, leaf, &parent, &why)) {
fprintf(stderr, "Error: \"%s\" %s.\n", asName, why);
goto failed;
}
if (!parentIsUsable(&directory, parent, asName)) {
goto failed;
}
if (findIn(&directory, leaf, parent) >= 0) {
fprintf(stderr, "Error: \"%s\" is already there. Delete it first.\n", asName);
goto failed; goto failed;
} }
int slot = -1; int slot = -1;
@@ -380,7 +914,8 @@ static int commandPut(const char *path, const char *hostFile, const char *asName
writeWord(entry + SBFS_ENTRY_START, (uint16_t)start); writeWord(entry + SBFS_ENTRY_START, (uint16_t)start);
writeWord(entry + SBFS_ENTRY_BLOCKS, whole); writeWord(entry + SBFS_ENTRY_BLOCKS, whole);
entry[SBFS_ENTRY_TAIL] = tail; entry[SBFS_ENTRY_TAIL] = tail;
memcpy(entry + SBFS_ENTRY_NAME, asName, strlen(asName)); memcpy(entry + SBFS_ENTRY_NAME, leaf, strlen(leaf));
entrySetParent(entry, parent);
super.freeBlocks = countFree(&directory, &super); super.freeBlocks = countFree(&directory, &super);
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) { if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
@@ -410,9 +945,16 @@ static int commandGet(const char *path, const char *name, const char *hostFile)
fclose(image); fclose(image);
return 1; return 1;
} }
int slot = findByName(&directory, name); int slot = -1;
if (slot < 0) { const char *why = NULL;
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name); if (resolve(&directory, name, &slot, &why) || slot < 0) {
fprintf(stderr, "Error: \"%s\" %s.\n", name, why ? why : "is the root, which is not a file");
free(directory.bytes);
fclose(image);
return 1;
}
if (entryIsDirectory(entryAt(&directory, slot))) {
fprintf(stderr, "Error: \"%s\" is a directory. There is nothing to take off.\n", name);
free(directory.bytes); free(directory.bytes);
fclose(image); fclose(image);
return 1; return 1;
@@ -460,9 +1002,18 @@ static int commandDelete(const char *path, const char *name) {
fclose(image); fclose(image);
return 1; return 1;
} }
int slot = findByName(&directory, name); int slot = -1;
if (slot < 0) { const char *why = NULL;
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name); if (resolve(&directory, name, &slot, &why) || slot < 0) {
fprintf(stderr, "Error: \"%s\" %s.\n", name, why ? why : "is the root, which cannot be deleted");
free(directory.bytes);
fclose(image);
return 1;
}
// delete is for files and rmdir is for directories, so that neither can be the one
// that took away more than was asked for.
if (entryIsDirectory(entryAt(&directory, slot))) {
fprintf(stderr, "Error: \"%s\" is a directory. Use rmdir.\n", name);
free(directory.bytes); free(directory.bytes);
fclose(image); fclose(image);
return 1; return 1;
@@ -481,19 +1032,142 @@ static int commandDelete(const char *path, const char *name) {
return failed; return failed;
} }
// ---- mkdir ----
//
// A directory costs one entry and no blocks at all. Its start, blocks and tail stay zero,
// which is what keeps it out of the allocator's way: with files laid down contiguously an
// entry with no range cannot overlap anything.
//
// This is also the only thing that raises a disk from version one to version two, because
// it is the only thing that makes the difference between them real.
static int commandMakeDirectory(const char *path, const char *name) {
FILE *image = openImage(path, "r+b");
if (image == NULL) {
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(image);
return 1;
}
char leaf[SBFS_NAME_BYTES + 1];
int parent = -1;
const char *why = NULL;
int failed = 1;
if (resolveParent(&directory, name, leaf, &parent, &why)) {
fprintf(stderr, "Error: \"%s\" %s.\n", name, why);
goto done;
}
if (!parentIsUsable(&directory, parent, name)) {
goto done;
}
if (findIn(&directory, leaf, parent) >= 0) {
fprintf(stderr, "Error: \"%s\" is already there.\n", name);
goto done;
}
int slot = -1;
for (int i = 0; i < directory.entries && slot < 0; i++) {
if (!entryInUse(entryAt(&directory, i))) {
slot = i;
}
}
if (slot < 0) {
fprintf(stderr, "Error: The directory is full: %d entries, all taken.\n",
directory.entries);
goto done;
}
uint8_t *entry = entryAt(&directory, slot);
memset(entry, 0, SBFS_ENTRY_BYTES);
entry[SBFS_ENTRY_FLAGS] = SBFS_FLAG_IN_USE | SBFS_FLAG_DIRECTORY;
memcpy(entry + SBFS_ENTRY_NAME, leaf, strlen(leaf));
entrySetParent(entry, parent);
int raised = (super.version < SBFS_VERSION_TREE);
super.version = SBFS_VERSION_TREE;
super.freeBlocks = countFree(&directory, &super);
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
goto done;
}
printf("Made \"%s\" as entry %d.\n", name, slot);
if (raised) {
printf("The disk is now version %d, because it has a directory on it.\n",
SBFS_VERSION_TREE);
}
failed = 0;
done:
free(directory.bytes);
fclose(image);
return failed;
}
// ---- rmdir ----
//
// Refuses a directory with anything in it, and that refusal is not politeness. Parents
// are entry indices, and a freed index is handed out again to the next thing put on the
// disk - so the children of a deleted directory would reappear inside whatever took its
// place. Emptying it first is the only safe order, and making the caller do that is the
// smallest way to guarantee it.
static int commandRemoveDirectory(const char *path, const char *name) {
FILE *image = openImage(path, "r+b");
if (image == NULL) {
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(image);
return 1;
}
int slot = -1;
const char *why = NULL;
int failed = 1;
if (resolve(&directory, name, &slot, &why)) {
fprintf(stderr, "Error: \"%s\" %s.\n", name, why);
goto done;
}
if (slot < 0) {
fprintf(stderr, "Error: The root is not something that can be removed.\n");
goto done;
}
if (!entryIsDirectory(entryAt(&directory, slot))) {
fprintf(stderr, "Error: \"%s\" is a file. Use delete.\n", name);
goto done;
}
if (directoryHasChildren(&directory, slot)) {
fprintf(stderr, "Error: \"%s\" still has things in it. Empty it first.\n", name);
goto done;
}
memset(entryAt(&directory, slot), 0, SBFS_ENTRY_BYTES);
super.freeBlocks = countFree(&directory, &super);
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
goto done;
}
printf("Removed \"%s\".\n", name);
failed = 0;
done:
free(directory.bytes);
fclose(image);
return failed;
}
static void printUsage(const char *program) { static void printUsage(const char *program) {
printf("Usage: %s <command> <image> [arguments]\n", program); printf("Usage: %s <command> <image> [arguments]\n", program);
printf("\n"); printf("\n");
printf("Commands:\n"); printf("Commands:\n");
printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n"); printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n");
printf(" list <image> Show what is on the disk.\n"); printf(" list <image> [path] Show the disk, or one directory of it.\n");
printf(" put <image> <file> [name] Put a host file onto it.\n"); printf(" put <image> <file> [path] Put a host file onto it.\n");
printf(" get <image> <name> [file] Take one off it.\n"); printf(" get <image> <path> [file] Take one off it.\n");
printf(" delete <image> <name> Remove one.\n"); printf(" delete <image> <path> Remove a file.\n");
printf(" mkdir <image> <path> Make a directory.\n");
printf(" rmdir <image> <path> Remove an empty one.\n");
printf("\n"); printf("\n");
printf("Blocks are %d bytes. A name may be %d characters. Without one, put uses the\n", printf("Blocks are %d bytes. A name may be %d characters, and a path is names with\n",
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES); SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
printf("file's own name, which is often too long, and it will say so.\n"); printf("'%c' between them, always from the root. Without a path, put uses the file's\n",
SBFS_SEPARATOR);
printf("own name, which is often too long, and it will say so.\n");
} }
// The part of a path after the last separator, so that put can default to a file's own // The part of a path after the last separator, so that put can default to a file's own
@@ -518,15 +1192,55 @@ int main(int argc, char *argv[]) {
if (strcmp(command, "format") == 0) { if (strcmp(command, "format") == 0) {
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512; long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS; long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
// Blocks in EACH boot slot, and there are two of them. Left off, a disk gets no
// boot area at all, which is what every disk made before this had.
long bootBlocks = (argc > 5) ? strtol(argv[5], NULL, 0) : 0;
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) { if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least" fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
" one of directory.\n"); " one of directory.\n");
return 1; return 1;
} }
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks); if (bootBlocks < 0 || bootBlocks * SBFS_BOOT_SLOTS > 0xFFFE) {
fprintf(stderr, "Error: A boot slot is between 0 and %d blocks, and there are"
" two of them.\n", 0xFFFE / SBFS_BOOT_SLOTS);
return 1;
}
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks,
(uint16_t)bootBlocks);
} }
if (strcmp(command, "list") == 0) { if (strcmp(command, "list") == 0) {
return commandList(path); return commandList(path, (argc > 3) ? argv[3] : NULL);
}
if (strcmp(command, "mkdir") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: mkdir needs a directory to make.\n");
return 1;
}
return commandMakeDirectory(path, argv[3]);
}
if (strcmp(command, "rmdir") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: rmdir needs a directory to remove.\n");
return 1;
}
return commandRemoveDirectory(path, argv[3]);
}
if (strcmp(command, "boot") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: boot needs a file to write into a slot.\n");
return 1;
}
return commandBoot(path, argv[3], (argc > 4) ? strtol(argv[4], NULL, 0) : 0);
}
if (strcmp(command, "bootstate") == 0) {
return commandBootState(path, (argc > 3) ? argv[3] : NULL);
}
if (strcmp(command, "bootslot") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: bootslot needs the slot to start from.\n");
return 1;
}
return commandBootSlot(path, strtol(argv[3], NULL, 0));
} }
if (strcmp(command, "put") == 0) { if (strcmp(command, "put") == 0) {
if (argc < 4) { if (argc < 4) {
+139 -6
View File
@@ -1,5 +1,5 @@
// sbfs.h // sbfs.h
// The SplitBit Filesystem, version one. // The SplitBit Filesystem, version two.
// //
// This is the host side's copy of the format. The other implementation is SplitBit // This is the host side's copy of the format. The other implementation is SplitBit
// assembly running on the machine itself, so nothing can be shared between them except // assembly running on the machine itself, so nothing can be shared between them except
@@ -19,7 +19,26 @@
#define SBFS_MAGIC "SBFS" #define SBFS_MAGIC "SBFS"
#define SBFS_MAGIC_BYTES 4 #define SBFS_MAGIC_BYTES 4
#define SBFS_VERSION 1
// ---- Versions ----
//
// Version two adds directories, and adds them without moving a single byte that version
// one had defined: the parent lives in two of the four bytes each entry already reserved,
// and a directory is a flag bit in a byte that was using one of its eight.
//
// A VERSION ONE DISK IS ALREADY A VALID VERSION TWO DISK. The parent field is written as
// the entry's index PLUS ONE, so that the zero a version one disk has in those reserved
// bytes reads as "in the root" - which is exactly what every file on a flat disk is in.
// There is nothing to convert and no tool to convert it with.
//
// Compatibility therefore runs one way, which is the ordinary shape of it: this reads
// either version, and version one code reading a version two disk would list directories
// as strange empty files. A disk is written back at the version it was read at, and is
// only raised to two by the thing that makes the difference real - the first directory
// created on it.
#define SBFS_VERSION_FLAT 1
#define SBFS_VERSION_TREE 2
#define SBFS_VERSION SBFS_VERSION_TREE
#define SBFS_BLOCK_BYTES 256 #define SBFS_BLOCK_BYTES 256
@@ -32,13 +51,66 @@
// 8 2 First directory block // 8 2 First directory block
// 10 2 Blocks the directory occupies // 10 2 Blocks the directory occupies
// 12 2 Free blocks, a cache rather than the authority // 12 2 Free blocks, a cache rather than the authority
// 14 Reserved to the end of the block // 14 2 Blocks in each boot slot, or zero for a disk that cannot be booted
// 16 1 Which boot slot is live, 0 or 1
// 17 1 How the last start went. See below.
// 18 Reserved to the end of the block
#define SBFS_SUPER_VERSION 4 #define SBFS_SUPER_VERSION 4
#define SBFS_SUPER_DISK 6 #define SBFS_SUPER_DISK 6
#define SBFS_SUPER_DIRSTART 8 #define SBFS_SUPER_DIRSTART 8
#define SBFS_SUPER_DIRBLOCKS 10 #define SBFS_SUPER_DIRBLOCKS 10
#define SBFS_SUPER_FREE 12 #define SBFS_SUPER_FREE 12
#define SBFS_SUPER_BOOTBLOCKS 14
#define SBFS_SUPER_BOOTSLOT 16
// ---- The boot area ----
//
// Blocks between the superblock and the directory, which the filesystem never allocates
// and never sees. Nothing had to be added to make room for them: both implementations
// work out the first usable data block as directoryStart + directoryBlocks, and
// directoryStart is a field rather than a constant, so moving the directory up reserves
// everything below it by arithmetic that was already there.
//
// A DISK MADE BEFORE THIS HAS ZERO HERE, which reads as "no boot area", which is true.
// The same shape as the version two parent field: the value an older disk already holds
// is the correct answer rather than something needing conversion.
//
// TWO SLOTS, ALWAYS, and the reason is that a boot slot is raw blocks. A file being
// rewritten is protected by writing a temporary and renaming it, and there is no name
// here to rename - so a machine interrupted while updating its only boot slot would not
// boot at all, which is the one failure on this disk with no way back. Writing the slot
// that is not live and then moving one byte turns that into a machine that boots the
// version it had before.
//
// block 0 the superblock
// 1 .. bootBlocks slot 0
// bootBlocks+1 .. 2*bootBlocks slot 1
// directoryStart .. the directory, and then files
// ---- How the last start went ----
//
// Written by the loader before it hands over and cleared by the system once it is running,
// so that a system which never gets that far leaves a mark saying so. THE MARK IS WHAT
// MAKES A NEW SYSTEM SAFE TO TRY: without it, pointing boot.cfg at something that crashes
// before the prompt is a machine that cannot be told anything ever again.
//
// What clears it is reaching the shell, and that is a deliberate choice of threshold. It
// does not mean the system works - a shell can be reached by something that is broken in
// every other way. It means A PERSON HAS CONTROL AGAIN, which is exactly what the fallback
// exists to restore and therefore exactly when it has done its job.
//
// 0 Settled. The last start finished. Start what the configuration says.
// 1 Trying. The loader handed over and nothing came back to say it arrived.
// 2 Fell back. A try failed and the fallback was used instead. Stays until somebody
// settles it, so that a system which crashes is not retried every other
// boot for ever.
#define SBFS_SUPER_BOOTSTATE 17
#define SBFS_BOOT_SETTLED 0
#define SBFS_BOOT_TRYING 1
#define SBFS_BOOT_FELLBACK 2
#define SBFS_BOOT_SLOTS 2
#define SBFS_FIRST_BOOT_BLOCK 1
// ---- Directory entries ---- // ---- Directory entries ----
// //
@@ -47,10 +119,22 @@
// 3 2 Whole blocks the file occupies // 3 2 Whole blocks the file occupies
// 5 1 Bytes in the trailing part block, or zero if there is not one // 5 1 Bytes in the trailing part block, or zero if there is not one
// 6 22 Name, padded with zeroes // 6 22 Name, padded with zeroes
// 28 4 Reserved // 28 2 Parent, as an entry index plus one. Zero is the root. (Version two.)
// 30 2 Reserved
// //
// Thirty two divides two hundred and fifty six, so an entry never straddles a block and // Thirty two divides two hundred and fifty six, so an entry never straddles a block and
// reading one never means handling a split. // reading one never means handling a split. Version two did not change that, because it
// spent bytes that were already inside the entry.
//
// A DIRECTORY IS AN ENTRY WITH NO BLOCKS. Its start, blocks and tail are all zero, and it
// costs one entry and nothing else. That is what keeps the flat array of entries the
// whole allocation map: with files laid down contiguously, every block is inside some
// entry's range or it is not, and an entry with no range is in nobody's way.
//
// Because parents are entry indices, and because nothing ever compacts the directory,
// those indices are stable for as long as an entry is in use. DELETING A DIRECTORY THAT
// STILL HAS CHILDREN MUST BE REFUSED: the freed index would be handed to some unrelated
// file later, and the orphans would reappear inside it.
#define SBFS_ENTRY_BYTES 32 #define SBFS_ENTRY_BYTES 32
#define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES) #define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES)
@@ -61,8 +145,57 @@
#define SBFS_ENTRY_TAIL 5 #define SBFS_ENTRY_TAIL 5
#define SBFS_ENTRY_NAME 6 #define SBFS_ENTRY_NAME 6
#define SBFS_NAME_BYTES 22 #define SBFS_NAME_BYTES 22
#define SBFS_ENTRY_PARENT 28
#define SBFS_FLAG_IN_USE 0x01 #define SBFS_FLAG_IN_USE 0x01
#define SBFS_FLAG_DIRECTORY 0x02
// A FILE BEING WRITTEN, WHICH IS NOT YET A FILE. Saving something that already exists is
// done by writing a temporary, deleting the original and giving the temporary its name,
// so that nothing is lost if the writing fails. The temporary has to be an ordinary entry
// while that happens - it holds real blocks and needs a name - and the only thing that
// distinguishes it from a finished file is that nobody has committed it yet.
//
// That is not a property of its contents. The same bytes become the real file the moment
// the rename lands, so there is nothing to put inside it that would be true; it belongs
// in the entry, which is the thing the commit changes. It was formerly told apart by
// being called "sbfs.part" or "sbfs.out", and those are legal names a user may also
// choose, so cleaning up by name could delete somebody's file.
//
// Cleared as part of committing. An entry still carrying it is the wreckage of a write
// that stopped, and its blocks are spoken for until something clears it up.
#define SBFS_FLAG_TEMPORARY 0x04
// The root is not an entry. It is the absence of a parent, written as zero, which is why
// the field is an index plus one and why a freshly zeroed entry is already in the root.
#define SBFS_PARENT_ROOT 0
#define SBFS_PARENT_OF(index) ((uint16_t)((index) + 1))
#define SBFS_PARENT_INDEX(parent) ((int)(parent) - 1)
// ---- How big a directory may be ----
//
// The parent is an index plus one in sixteen bits, so index 65535 has no representation:
// adding one wraps to zero, and zero is the root. An entry that cannot be named as a
// parent is a directory that cannot hold anything, and it does not fail by refusing.
//
// WHAT IT DOES INSTEAD IS WORSE THAN FAILING. Creating something inside it writes a
// parent of zero, so the thing lands in the root while the tool reports the path it was
// asked for. Looking in that directory afterwards finds nothing, because the search is
// for a parent of 65536 and the entry says zero - so the same create succeeds again, and
// again, piling up entries of one name 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.
//
// Eight entries to a block, and 65535 entries is the most that leaves every index one
// short of the wrap. 8191 blocks gives 65528 of them, which is the last whole block that
// fits. Checked when formatting and again when reading, because a disk claiming more may
// have been made by something that never checked at all.
#define SBFS_MAX_DIRECTORY_BLOCKS 8191
#define SBFS_MAX_ENTRIES (SBFS_MAX_DIRECTORY_BLOCKS * SBFS_ENTRIES_PER_BLOCK)
// Paths are separated by this, and a leading one means "from the root". A name may not
// contain it, which is what makes a path unambiguous without any quoting.
#define SBFS_SEPARATOR '/'
// The directory begins at block 1 and is this many blocks unless told otherwise, which // The directory begins at block 1 and is this many blocks unless told otherwise, which
// is sixty four files. The superblock carries the real number, so this is only what a // is sixty four files. The superblock carries the real number, so this is only what a
+35 -6
View File
@@ -6,6 +6,7 @@
#include "bootstrap.h" #include "bootstrap.h"
#include "../Assembler/assembly.h" // For the boot image format, which both tools share. #include "../Assembler/assembly.h" // For the boot image format, which both tools share.
#include <stdio.h> #include <stdio.h>
#include <stdint.h>
#include <string.h> #include <string.h>
// Reads a number of the given width, most significant byte first. // Reads a number of the given width, most significant byte first.
@@ -144,18 +145,46 @@ static uint8_t readVectorSegment(FILE *file, uint8_t *Program) {
return 0; return 0;
} }
// The Program Segment must come first, then the Data Segment. Short circuiting here means
// there is one exit, and so only one place that has to close whatever it was reading.
static uint8_t readImage(FILE *file, uint8_t *Program, uint8_t *Data) {
return readFileHeader(file)
|| readSegment(file, "PRG", Program)
|| readSegment(file, "DAT", Data)
|| readVectorSegment(file, Program);
}
// ---- Waking up in ROM ----
//
// The same reader as a named file, given the bytes instead of a path, because a ROM is a
// boot image and there is no reason for the machine to have two ways of understanding one.
//
// THIS IS SHADOWING, which is a real technique rather than a convenience: reset copies the
// ROM into Program Memory, including its boot vector, and the CPU then does exactly what it
// has always done - reads the boot vector and starts where it points. Nothing about the CPU
// changes to make a machine that starts itself.
//
// And because it is a copy rather than a mapping, the bytes are ordinary Program Memory
// once stage one has jumped away. The system may write over them; a reset puts them back.
uint8_t loadROM(const unsigned char *bytes, unsigned long length,
uint8_t *Program, uint8_t *Data) {
FILE *file = fmemopen((void *)(uintptr_t)bytes, (size_t)length, "rb");
if (file == NULL) {
fprintf(stderr, "Error: Couldn't open the boot ROM.\n");
return 1;
}
uint8_t failed = readImage(file, Program, Data);
fclose(file);
return failed;
}
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) { uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
FILE *file = fopen(path, "rb"); FILE *file = fopen(path, "rb");
if (file == NULL) { if (file == NULL) {
fprintf(stderr, "Error: Couldn't open file: %s\n", path); fprintf(stderr, "Error: Couldn't open file: %s\n", path);
return 1; return 1;
} }
// The Program Segment must come first, then the Data Segment. Short circuiting uint8_t failed = readImage(file, Program, Data);
// here means there is one exit, and so only one place that has to close the file.
uint8_t failed = readFileHeader(file)
|| readSegment(file, "PRG", Program)
|| readSegment(file, "DAT", Data)
|| readVectorSegment(file, Program);
fclose(file); fclose(file);
return failed; return failed;
} }
+4
View File
@@ -10,4 +10,8 @@
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data); uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data);
// The same, from bytes the emulator carries rather than a file it opens. See loadROM.
uint8_t loadROM(const unsigned char *bytes, unsigned long length,
uint8_t *Program, uint8_t *Data);
#endif // BOOTSTRAP_H #endif // BOOTSTRAP_H
+15
View File
@@ -152,6 +152,15 @@ static int rangeWritable(uint8_t bank, uint16_t address, uint32_t count) {
return 1; return 1;
} }
// What the moves below have cost since anybody last asked.
static unsigned long pendingCycles = 0;
unsigned long controllerTakeCycles(void) {
unsigned long taken = pendingCycles;
pendingCycles = 0;
return taken;
}
static void doBlit(void) { static void doBlit(void) {
uint32_t count = transferLength(); uint32_t count = transferLength();
if (!rangeReadable(sourceBank, sourceAddress, count)) { if (!rangeReadable(sourceBank, sourceAddress, count)) {
@@ -166,6 +175,9 @@ static void doBlit(void) {
// designing against. // designing against.
memmove(banks[destBank].memory + destAddress, memmove(banks[destBank].memory + destAddress,
banks[sourceBank].memory + sourceAddress, count); banks[sourceBank].memory + sourceAddress, count);
// A byte read and a byte written. Two banks are two memories and the pair overlaps;
// one bank is one memory and they do not. The odd cycle is the pipeline filling.
pendingCycles += (sourceBank == destBank) ? 2 * count + 1 : count + 1;
sourceAddress = (uint16_t)(sourceAddress + count); sourceAddress = (uint16_t)(sourceAddress + count);
destAddress = (uint16_t)(destAddress + count); destAddress = (uint16_t)(destAddress + count);
status = 0; status = 0;
@@ -178,6 +190,7 @@ static void doFill(void) {
} }
// A fill has nowhere to read from, only a value, so SourceLow carries the byte and // A fill has nowhere to read from, only a value, so SourceLow carries the byte and
// the rest of the source registers mean nothing here. // the rest of the source registers mean nothing here.
pendingCycles += count + 1;
memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count); memset(banks[destBank].memory + destAddress, (int)(sourceAddress & 0xFF), count);
destAddress = (uint16_t)(destAddress + count); destAddress = (uint16_t)(destAddress + count);
status = 0; status = 0;
@@ -261,6 +274,7 @@ uint8_t controllerWrite(uint8_t value, uint8_t port) {
// A byte into the destination, and the address steps on so that writing a // A byte into the destination, and the address steps on so that writing a
// run of bytes is a loop over one instruction rather than four. // run of bytes is a loop over one instruction rather than four.
if (canWrite(destBank, destAddress)) { if (canWrite(destBank, destAddress)) {
pendingCycles++; // The byte itself, beyond reaching the port.
banks[destBank].memory[destAddress] = value; banks[destBank].memory[destAddress] = value;
if (destBank == BANK_TABLE) { if (destBank == BANK_TABLE) {
// Unreachable while the table is read only, and here so that it stays // Unreachable while the table is read only, and here so that it stays
@@ -321,6 +335,7 @@ uint8_t controllerRead(uint8_t port) {
if (!canRead(sourceBank, sourceAddress)) { if (!canRead(sourceBank, sourceAddress)) {
return 0; return 0;
} }
pendingCycles++; // As above, the other way round.
uint8_t value = banks[sourceBank].memory[sourceAddress]; uint8_t value = banks[sourceBank].memory[sourceAddress];
sourceAddress++; sourceAddress++;
status = 0; status = 0;
+19
View File
@@ -85,6 +85,25 @@
// Banks 0 to 2 belong to the machine rather than to any device. // Banks 0 to 2 belong to the machine rather than to any device.
#define BANK_OWNER_MACHINE 0xFF #define BANK_OWNER_MACHINE 0xFF
// ---- What the controller's work costs ----
//
// The controller moves memory, and memory takes time to move: a byte has to be read from
// somewhere and written somewhere else. A blit is not free just because the machine issues
// it with one instruction, and pretending otherwise made a quarter of a millisecond of
// work look like ten cycles.
//
// Banks are separate memories, which is what decides the rate. A move between two of them
// can overlap its read and its write - fetch the next byte while the last one is stored -
// so it settles at a byte a cycle. A move WITHIN one bank cannot, and costs two. A fill has
// nothing to read and costs one whatever the banks are.
//
// Returned and cleared, so the caller adds it to whatever it is charging for. The CPU picks
// it up after each port access, which makes the transfer a stall: the machine issues a blit
// and waits for it. Whether real hardware would let the two run at once is a live question -
// the memories are separate, so it plausibly could - and the answer wants measuring before
// it is designed.
unsigned long controllerTakeCycles(void);
void initializeController(uint8_t *programMemory, uint8_t *dataMemory); void initializeController(uint8_t *programMemory, uint8_t *dataMemory);
uint8_t controllerWrite(uint8_t value, uint8_t port); uint8_t controllerWrite(uint8_t value, uint8_t port);
+277 -85
View File
@@ -5,12 +5,52 @@
#include "cpu.h" #include "cpu.h"
#include "io.h" #include "io.h"
#include "controller.h"
#include "../Assembler/assembly.h" // For the vector table layout, which both tools share. #include "../Assembler/assembly.h" // For the vector table layout, which both tools share.
uint16_t shiftRegister; uint16_t shiftRegister;
// Reads one entry out of a vector table. Most significant byte first, matching the // Reads one entry out of a vector table. Most significant byte first, matching the
// branch instructions and both file formats. // branch instructions and both file formats.
// ---- Every touch of memory, and what it costs ----
//
// One access, one cycle. These exist so that the cost is counted in the one place the
// access happens, rather than in a table of per instruction costs kept somewhere else -
// a table like that is a second copy of what the code does, and the two drift.
static inline uint8_t fetchProgram(CPURegisters *cpu, uint16_t at) {
cpu->busCycles++;
return cpu->Program[at];
}
static inline uint8_t readData(CPURegisters *cpu, uint16_t at) {
cpu->busCycles++;
return cpu->Data[at];
}
static inline void writeData(CPURegisters *cpu, uint16_t at, uint8_t value) {
cpu->busCycles++;
cpu->Data[at] = value;
}
// A device port is reached over the same bus as memory, so it costs the same. That is a
// claim about the hardware rather than an observation - a machine could put devices
// somewhere faster or slower - and it is the simple thing until there is a reason to say
// otherwise.
static inline void portOut(CPURegisters *cpu, uint8_t value, uint8_t port) {
cpu->busCycles++;
OutputHandler(value, port);
// And whatever memory that made the controller move. The machine waits for it, which
// is the conservative reading: a blit stalls the program that asked for one.
cpu->busCycles += controllerTakeCycles();
}
static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) {
cpu->busCycles++;
uint8_t value = InputHandler(port);
cpu->busCycles += controllerTakeCycles();
return value;
}
static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t index) { static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t index) {
uint16_t address = base + (uint16_t)index * VECTOR_ENTRY_BYTES; uint16_t address = base + (uint16_t)index * VECTOR_ENTRY_BYTES;
return ((uint16_t)programMemory[address] << 8) | (uint16_t)programMemory[address + 1]; return ((uint16_t)programMemory[address] << 8) | (uint16_t)programMemory[address + 1];
@@ -38,23 +78,23 @@ static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, u
} }
// Order mirrors genericCall exactly: low byte then high byte, lowest numbered Data // Order mirrors genericCall exactly: low byte then high byte, lowest numbered Data
// Pointer first, so that anything walking the Stack sees a familiar shape. // Pointer first, so that anything walking the Stack sees a familiar shape.
cpu->Data[cpu->StackPointer] = resumeAddress & 0xFF; writeData(cpu, cpu->StackPointer, resumeAddress & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (resumeAddress >> 8) & 0xFF; writeData(cpu, cpu->StackPointer, (resumeAddress >> 8) & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
for (int i = 0; i < DATA_POINTERS; i++) { for (int i = 0; i < DATA_POINTERS; i++) {
cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF; writeData(cpu, cpu->StackPointer, cpu->DataPointer[i] & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF; writeData(cpu, cpu->StackPointer, (cpu->DataPointer[i] >> 8) & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
} }
cpu->Data[cpu->StackPointer] = cpu->B; writeData(cpu, cpu->StackPointer, cpu->B);
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = cpu->A; writeData(cpu, cpu->StackPointer, cpu->A);
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = cpu->Q; writeData(cpu, cpu->StackPointer, cpu->Q);
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = cpu->Status; writeData(cpu, cpu->StackPointer, cpu->Status);
cpu->StackPointer--; cpu->StackPointer--;
// A handler runs with hardware interrupts held off unless it says otherwise, so an // A handler runs with hardware interrupts held off unless it says otherwise, so an
// interrupt cannot arrive inside the handler for another one and grow the Stack // interrupt cannot arrive inside the handler for another one and grow the Stack
@@ -103,6 +143,9 @@ void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemor
cpu->StackPointer = 0xFFFF; cpu->StackPointer = 0xFFFF;
cpu->Program = programMemory; cpu->Program = programMemory;
cpu->Data = dataMemory; cpu->Data = dataMemory;
cpu->busCycles = 0;
cpu->idleCycles = 0;
cpu->Waiting = 0;
cpu->Fault = FAULT_NONE; cpu->Fault = FAULT_NONE;
cpu->FaultVector = 0; cpu->FaultVector = 0;
} }
@@ -112,33 +155,33 @@ void genericBranch(CPURegisters *cpu){
// Byte order is imporant. Most Significant first, then Least Significant. // Byte order is imporant. Most Significant first, then Least Significant.
cpu->ProgramCounter++; // Move to the next byte. (MSB) cpu->ProgramCounter++; // Move to the next byte. (MSB)
uint16_t DestinationAddress; uint16_t DestinationAddress;
DestinationAddress = (uint16_t)cpu->Program[cpu->ProgramCounter] << 8; // Cast the 8 bit value to a 16 bit value and shifts it up to the high byte. DestinationAddress = (uint16_t)fetchProgram(cpu, cpu->ProgramCounter) << 8; // Cast the 8 bit value to a 16 bit value and shifts it up to the high byte.
cpu->ProgramCounter++; // Move to the next byte. (LSB) cpu->ProgramCounter++; // Move to the next byte. (LSB)
DestinationAddress = DestinationAddress | (uint16_t)cpu->Program[cpu->ProgramCounter]; // Cast the 8 bit value to a 16 bit value and or it to add it to the desination. DestinationAddress = DestinationAddress | (uint16_t)fetchProgram(cpu, cpu->ProgramCounter); // Cast the 8 bit value to a 16 bit value and or it to add it to the desination.
cpu->ProgramCounter = DestinationAddress-1; cpu->ProgramCounter = DestinationAddress-1;
} }
void genericCall(CPURegisters *cpu){ void genericCall(CPURegisters *cpu){
// Order, low byte, high byte // Order, low byte, high byte
cpu->Data[cpu->StackPointer] = cpu->ProgramCounter & 0xFF; writeData(cpu, cpu->StackPointer, cpu->ProgramCounter & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (cpu->ProgramCounter >> 8) & 0xFF; writeData(cpu, cpu->StackPointer, (cpu->ProgramCounter >> 8) & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
// Push the preserved Data Pointers to the Stack, lowest numbered first. // Push the preserved Data Pointers to the Stack, lowest numbered first.
// Order within each one, low byte, high byte. // Order within each one, low byte, high byte.
// The pointers above PRESERVED_DATA_POINTERS are deliberately left alone, so a // The pointers above PRESERVED_DATA_POINTERS are deliberately left alone, so a
// subroutine can use one to hand an address back to whoever called it. // subroutine can use one to hand an address back to whoever called it.
for (int i = 0; i < PRESERVED_DATA_POINTERS; i++) { for (int i = 0; i < PRESERVED_DATA_POINTERS; i++) {
cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF; writeData(cpu, cpu->StackPointer, cpu->DataPointer[i] & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF; writeData(cpu, cpu->StackPointer, (cpu->DataPointer[i] >> 8) & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
} }
// Push B to the Stack. // Push B to the Stack.
cpu->Data[cpu->StackPointer] = cpu->B; writeData(cpu, cpu->StackPointer, cpu->B);
cpu->StackPointer--; cpu->StackPointer--;
// Push A to the Stack. // Push A to the Stack.
cpu->Data[cpu->StackPointer] = cpu->A; writeData(cpu, cpu->StackPointer, cpu->A);
cpu->StackPointer--; cpu->StackPointer--;
// Perform a Generic Branch to the Address. // Perform a Generic Branch to the Address.
genericBranch(cpu); genericBranch(cpu);
@@ -150,7 +193,7 @@ uint16_t *selectDataPointer(CPURegisters *cpu) {
// rather than rejected, the way a narrow field in hardware would be. It is the // rather than rejected, the way a narrow field in hardware would be. It is the
// assembler's job to refuse to emit one in the first place. // assembler's job to refuse to emit one in the first place.
cpu->ProgramCounter++; cpu->ProgramCounter++;
return &cpu->DataPointer[cpu->Program[cpu->ProgramCounter] & (DATA_POINTERS - 1)]; return &cpu->DataPointer[fetchProgram(cpu, cpu->ProgramCounter) & (DATA_POINTERS - 1)];
} }
uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
@@ -160,7 +203,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
uint16_t result; uint16_t result;
switch(Instruction) { switch(Instruction) {
// 0x - Arithmetic and Logic Operations. // 0x - Arithmetic and Logic Operations.
case 0x00: case 0x10:
// ADD - A + B + Carry -> Q // ADD - A + B + Carry -> Q
result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & STATUS_CARRY); result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & STATUS_CARRY);
if (result > 255) { if (result > 255) {
@@ -170,7 +213,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
} }
cpu->Q = result & 0xFF; cpu->Q = result & 0xFF;
break; break;
case 0x01: case 0x11:
// SUB - A - B - Carry -> Q // SUB - A - B - Carry -> Q
result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & STATUS_CARRY); result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & STATUS_CARRY);
if (result > 255) { if (result > 255) {
@@ -180,34 +223,34 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
} }
cpu->Q = result & 0xFF; cpu->Q = result & 0xFF;
break; break;
case 0x02: case 0x12:
// AND - A and B -> Q // AND - A and B -> Q
cpu->Q = cpu->A&cpu->B; cpu->Q = cpu->A&cpu->B;
break; break;
case 0x03: case 0x13:
// OR - A or B -> Q // OR - A or B -> Q
cpu->Q = cpu->A|cpu->B; cpu->Q = cpu->A|cpu->B;
break; break;
case 0x04: case 0x14:
// XOR - A xor B -> Q // XOR - A xor B -> Q
cpu->Q = cpu->A^cpu->B; cpu->Q = cpu->A^cpu->B;
break; break;
case 0x05: case 0x15:
// NOTA - not A -> Q // NOTA - not A -> Q
cpu->Q = ~cpu->A; cpu->Q = ~cpu->A;
break; break;
case 0x06: case 0x16:
// NOTB - not B -> Q // NOTB - not B -> Q
cpu->Q = ~cpu->B; cpu->Q = ~cpu->B;
break; break;
case 0x07: case 0x17:
// SHL - Shift AB left. // SHL - Shift AB left.
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B; shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
shiftRegister = (shiftRegister << 1) | (shiftRegister >> 15); shiftRegister = (shiftRegister << 1) | (shiftRegister >> 15);
cpu->A = shiftRegister >> 8; cpu->A = shiftRegister >> 8;
cpu->B = shiftRegister & 0xFF; cpu->B = shiftRegister & 0xFF;
break; break;
case 0x08: case 0x18:
// SHR - Shift AB right. // SHR - Shift AB right.
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B; shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
shiftRegister = (shiftRegister >> 1) | (shiftRegister << 15); shiftRegister = (shiftRegister >> 1) | (shiftRegister << 15);
@@ -217,11 +260,11 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// //
// 1x - Branch Operations: // 1x - Branch Operations:
// //
case 0x10: case 0x60:
// BRI - Branch Immediately // BRI - Branch Immediately
genericBranch(cpu); genericBranch(cpu);
break; break;
case 0x11: case 0x61:
// BRQ - Branch if Q = 0 // BRQ - Branch if Q = 0
if(cpu->Q == 0) { if(cpu->Q == 0) {
genericBranch(cpu); genericBranch(cpu);
@@ -230,7 +273,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2; cpu->ProgramCounter+=2;
} }
break; break;
case 0x12: case 0x62:
// BRA - Branch if A = 0 // BRA - Branch if A = 0
if(cpu->A == 0) { if(cpu->A == 0) {
genericBranch(cpu); genericBranch(cpu);
@@ -239,7 +282,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2; cpu->ProgramCounter+=2;
} }
break; break;
case 0x13: case 0x63:
// BRB - if B = 0 // BRB - if B = 0
if(cpu->B == 0) { if(cpu->B == 0) {
@@ -248,7 +291,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2; cpu->ProgramCounter+=2;
} }
break; break;
case 0x14: case 0x64:
// BRC - Do an immediate branch if the Carry Flag is set. // BRC - Do an immediate branch if the Carry Flag is set.
if (cpu->Status & STATUS_CARRY) { if (cpu->Status & STATUS_CARRY) {
genericBranch(cpu); genericBranch(cpu);
@@ -256,7 +299,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2; cpu->ProgramCounter+=2;
} }
break; break;
case 0x15: { case 0x65: {
// BRD - Branch to the address held in a Data Pointer. // BRD - Branch to the address held in a Data Pointer.
// This is the only branch whose destination is not written into the // This is the only branch whose destination is not written into the
// program, which is what makes a table of addresses something a program // program, which is what makes a table of addresses something a program
@@ -266,11 +309,29 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter = destination - 1; cpu->ProgramCounter = destination - 1;
} }
break; break;
case 0x17: case 0x70:
// RCAL - Call, pushing nothing but the return address.
//
// The unsafe one, and it says so in its name. CALL puts A, B and the first
// three Data Pointers back the way it found them, which costs ten bytes of
// Stack and means a subroutine can only hand anything back through Q, DP3 or
// memory. RCAL costs two bytes and puts nothing back at all: everything the
// callee touches, the caller has lost.
//
// It must be returned from with RRET. The two frames are different sizes, so
// returning from one through the other walks the Stack to somewhere that was
// never a return address.
writeData(cpu, cpu->StackPointer, cpu->ProgramCounter & 0xFF);
cpu->StackPointer--;
writeData(cpu, cpu->StackPointer, (cpu->ProgramCounter >> 8) & 0xFF);
cpu->StackPointer--;
genericBranch(cpu);
break;
case 0x71:
// CALL - Push the Program Counter to the Stack, and perform an immediate branch. // CALL - Push the Program Counter to the Stack, and perform an immediate branch.
genericCall(cpu); genericCall(cpu);
break; break;
case 0x1A: case 0x66:
// BNQ - Branch if Q is not 0. // BNQ - Branch if Q is not 0.
if(cpu->Q != 0) { if(cpu->Q != 0) {
genericBranch(cpu); genericBranch(cpu);
@@ -278,7 +339,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2; cpu->ProgramCounter+=2;
} }
break; break;
case 0x1B: case 0x67:
// BNA - Branch if A is not 0. // BNA - Branch if A is not 0.
if(cpu->A != 0) { if(cpu->A != 0) {
genericBranch(cpu); genericBranch(cpu);
@@ -286,7 +347,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2; cpu->ProgramCounter+=2;
} }
break; break;
case 0x1C: case 0x68:
// BNB - Branch if B is not 0. // BNB - Branch if B is not 0.
if(cpu->B != 0) { if(cpu->B != 0) {
genericBranch(cpu); genericBranch(cpu);
@@ -294,7 +355,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2; cpu->ProgramCounter+=2;
} }
break; break;
case 0x1D: case 0x69:
// BNC - Branch if the Carry Flag is clear. // BNC - Branch if the Carry Flag is clear.
if (!(cpu->Status & STATUS_CARRY)) { if (!(cpu->Status & STATUS_CARRY)) {
genericBranch(cpu); genericBranch(cpu);
@@ -302,13 +363,13 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter+=2; cpu->ProgramCounter+=2;
} }
break; break;
case 0x18: { case 0x72: {
// SWI - Software Interrupt. The byte after the opcode names the vector. // SWI - Software Interrupt. The byte after the opcode names the vector.
// Never masked: this is an instruction the program deliberately ran, not // Never masked: this is an instruction the program deliberately ran, not
// something a device asked for. // something a device asked for.
uint16_t site = cpu->ProgramCounter; uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++; cpu->ProgramCounter++;
uint8_t vector = cpu->Program[cpu->ProgramCounter]; uint8_t vector = fetchProgram(cpu, cpu->ProgramCounter);
// Execution resumes after the operand, which the Program Counter is sitting // Execution resumes after the operand, which the Program Counter is sitting
// on, so the resume address is one further on than that. // on, so the resume address is one further on than that.
if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, vector, cpu->ProgramCounter + 1)) { if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, vector, cpu->ProgramCounter + 1)) {
@@ -317,55 +378,111 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->ProgramCounter = site - 1; cpu->ProgramCounter = site - 1;
} }
} break; } break;
case 0x19: { case 0x73: {
// RETI - Return from an interrupt. Pops the frame in the exact reverse of // RETI - Return from an interrupt. Pops the frame in the exact reverse of
// the order enterInterrupt pushed it. // the order enterInterrupt pushed it.
cpu->StackPointer++; cpu->StackPointer++;
cpu->Status = cpu->Data[cpu->StackPointer]; cpu->Status = readData(cpu, cpu->StackPointer);
cpu->StackPointer++; cpu->StackPointer++;
cpu->Q = cpu->Data[cpu->StackPointer]; cpu->Q = readData(cpu, cpu->StackPointer);
cpu->StackPointer++; cpu->StackPointer++;
cpu->A = cpu->Data[cpu->StackPointer]; cpu->A = readData(cpu, cpu->StackPointer);
cpu->StackPointer++; cpu->StackPointer++;
cpu->B = cpu->Data[cpu->StackPointer]; cpu->B = readData(cpu, cpu->StackPointer);
for (int i = DATA_POINTERS - 1; i >= 0; i--) { for (int i = DATA_POINTERS - 1; i >= 0; i--) {
cpu->StackPointer++; cpu->StackPointer++;
cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8; cpu->DataPointer[i] = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++; cpu->StackPointer++;
cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer]; cpu->DataPointer[i] |= (uint16_t)readData(cpu, cpu->StackPointer);
} }
uint16_t resumeAddress; uint16_t resumeAddress;
cpu->StackPointer++; cpu->StackPointer++;
resumeAddress = (uint16_t)cpu->Data[cpu->StackPointer] << 8; resumeAddress = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++; cpu->StackPointer++;
resumeAddress = resumeAddress | (uint16_t)cpu->Data[cpu->StackPointer]; resumeAddress = resumeAddress | (uint16_t)readData(cpu, cpu->StackPointer);
// The frame holds the address to carry on from. The Program Counter is // The frame holds the address to carry on from. The Program Counter is
// stepped after every instruction, so land one short of it. RET does the // stepped after every instruction, so land one short of it. RET does the
// same job with its +2, for the same reason. // same job with its +2, for the same reason.
cpu->ProgramCounter = resumeAddress - 1; cpu->ProgramCounter = resumeAddress - 1;
} break; } break;
case 0x1F: case 0x76: {
// SRET - Return from a handler that has an answer.
//
// THE SAME FRAME AS RETI, WITH THE CALL CONVENTION'S RULE APPLIED TO IT. CALL
// saves A, B and Data Pointers 0 to 2 and nothing else, which is exactly why
// Q and DP3 are how a subroutine hands something back. An interrupt saves all
// of it, so a handler with an answer had to reach into its own frame and
// un-save two fields by hand - thirty places in CosmOS did that, each of them
// knowing the frame's layout by an offset, and all thirty would have gone
// quietly wrong the day the frame gained a field.
//
// So: RETI restores everything and is how a hardware handler says it was never
// here. SRET restores what a RET restores and is how a service says it has
// replied. The saved Q and DP3 are stepped over and dropped.
cpu->StackPointer++;
uint8_t savedStatus = readData(cpu, cpu->StackPointer);
// The Interrupt Flag, and only that. Entering a handler clears it and the
// frame is what puts it back, so dropping the whole byte would leave a service
// silently turning interrupts off. Everything else in Status - the carry
// above all - is left as the handler leaves it, because that is what a RET
// does and the point of this instruction is that there is one rule.
cpu->Status = (uint8_t)((cpu->Status & ~STATUS_INTERRUPT)
| (savedStatus & STATUS_INTERRUPT));
cpu->StackPointer++; // The saved Q, dropped: the handler's answer stands.
cpu->StackPointer++;
cpu->A = readData(cpu, cpu->StackPointer);
cpu->StackPointer++;
cpu->B = readData(cpu, cpu->StackPointer);
for (int i = DATA_POINTERS - 1; i >= 0; i--) {
cpu->StackPointer++;
uint16_t high = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++;
uint16_t low = (uint16_t)readData(cpu, cpu->StackPointer);
// Data Pointer 3 is stepped over for the same reason as Q. The other three
// come back, exactly as a RET brings them back.
if (i != DATA_POINTERS - 1) {
cpu->DataPointer[i] = high | low;
}
}
uint16_t resumeAddress;
cpu->StackPointer++;
resumeAddress = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++;
resumeAddress = resumeAddress | (uint16_t)readData(cpu, cpu->StackPointer);
cpu->ProgramCounter = resumeAddress - 1;
} break;
case 0x74:
// RRET - Return from an RCAL, taking back nothing but the return address.
cpu->StackPointer++;
cpu->ProgramCounter = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++;
cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)readData(cpu, cpu->StackPointer);
// Two on, to step over the address the RCAL branched through, exactly as RET
// does. Everything else RET restores, this deliberately does not.
cpu->ProgramCounter += 2;
break;
case 0x75:
// RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address. // RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address.
// Pop A from the Stack. // Pop A from the Stack.
cpu->StackPointer++; cpu->StackPointer++;
cpu->A = cpu->Data[cpu->StackPointer]; cpu->A = readData(cpu, cpu->StackPointer);
// Pop B from the Stack. // Pop B from the Stack.
cpu->StackPointer++; cpu->StackPointer++;
cpu->B = cpu->Data[cpu->StackPointer]; cpu->B = readData(cpu, cpu->StackPointer);
// Pop the preserved Data Pointers from the Stack. This walks the pointers // Pop the preserved Data Pointers from the Stack. This walks the pointers
// in the opposite order to genericCall, and takes the high byte before the // in the opposite order to genericCall, and takes the high byte before the
// low byte, so that it exactly mirrors the way they were pushed. // low byte, so that it exactly mirrors the way they were pushed.
for (int i = PRESERVED_DATA_POINTERS - 1; i >= 0; i--) { for (int i = PRESERVED_DATA_POINTERS - 1; i >= 0; i--) {
cpu->StackPointer++; cpu->StackPointer++;
cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8; cpu->DataPointer[i] = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++; cpu->StackPointer++;
cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer]; cpu->DataPointer[i] |= (uint16_t)readData(cpu, cpu->StackPointer);
} }
// Pop the Return Address from the Stack. // Pop the Return Address from the Stack.
cpu->StackPointer++; cpu->StackPointer++;
cpu->ProgramCounter = (uint16_t)cpu->Data[cpu->StackPointer] << 8; cpu->ProgramCounter = (uint16_t)readData(cpu, cpu->StackPointer) << 8;
cpu->StackPointer++; cpu->StackPointer++;
cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)cpu->Data[cpu->StackPointer]; cpu->ProgramCounter = cpu->ProgramCounter | (uint16_t)readData(cpu, cpu->StackPointer);
// Add 2 to the Program Counter to skip over the address when it returns. // Add 2 to the Program Counter to skip over the address when it returns.
cpu->ProgramCounter += 2; cpu->ProgramCounter += 2;
@@ -424,12 +541,12 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
case 0x26: case 0x26:
// INIA - Initialize A Immediately from Program Memory. // INIA - Initialize A Immediately from Program Memory.
cpu->ProgramCounter++; cpu->ProgramCounter++;
cpu->A = cpu->Program[cpu->ProgramCounter]; cpu->A = fetchProgram(cpu, cpu->ProgramCounter);
break; break;
case 0x27: case 0x27:
// INIB - Initialize A Immediately from Program Memory. // INIB - Initialize A Immediately from Program Memory.
cpu->ProgramCounter++; cpu->ProgramCounter++;
cpu->B = cpu->Program[cpu->ProgramCounter]; cpu->B = fetchProgram(cpu, cpu->ProgramCounter);
break; break;
case 0x28: case 0x28:
// CCF - Clear the Carry Flag. // CCF - Clear the Carry Flag.
@@ -458,17 +575,17 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// //
case 0x30: case 0x30:
// PSHQ - Push Q to the Stack. // PSHQ - Push Q to the Stack.
cpu->Data[cpu->StackPointer] = cpu->Q; writeData(cpu, cpu->StackPointer, cpu->Q);
cpu->StackPointer--; cpu->StackPointer--;
break; break;
case 0x31: case 0x31:
// PSHA - Push A to the Stack. // PSHA - Push A to the Stack.
cpu->Data[cpu->StackPointer] = cpu->A; writeData(cpu, cpu->StackPointer, cpu->A);
cpu->StackPointer--; cpu->StackPointer--;
break; break;
case 0x32: case 0x32:
// PSHB - Push B to the Stack. // PSHB - Push B to the Stack.
cpu->Data[cpu->StackPointer] = cpu->B; writeData(cpu, cpu->StackPointer, cpu->B);
cpu->StackPointer--; cpu->StackPointer--;
break; break;
case 0x33: { case 0x33: {
@@ -476,30 +593,30 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// Order, high byte, low byte // Order, high byte, low byte
// This ordering makes it easier to add offsets with register math. // This ordering makes it easier to add offsets with register math.
uint16_t pushed = *selectDataPointer(cpu); uint16_t pushed = *selectDataPointer(cpu);
cpu->Data[cpu->StackPointer] = (pushed >> 8) & 0xFF; writeData(cpu, cpu->StackPointer, (pushed >> 8) & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = pushed & 0xFF; writeData(cpu, cpu->StackPointer, pushed & 0xFF);
cpu->StackPointer--; cpu->StackPointer--;
} }
break; break;
case 0x34: case 0x34:
// POPA - Pop A from the Stack. // POPA - Pop A from the Stack.
cpu->StackPointer++; cpu->StackPointer++;
cpu->A = cpu->Data[cpu->StackPointer]; cpu->A = readData(cpu, cpu->StackPointer);
break; break;
case 0x35: case 0x35:
// POPB - Pop B from the Stack. // POPB - Pop B from the Stack.
cpu->StackPointer++; cpu->StackPointer++;
cpu->B = cpu->Data[cpu->StackPointer]; cpu->B = readData(cpu, cpu->StackPointer);
break; break;
case 0x36: { case 0x36: {
// POPD - Pop a Data Address from the Stack into the selected Data Pointer. // POPD - Pop a Data Address from the Stack into the selected Data Pointer.
uint16_t *popped = selectDataPointer(cpu); uint16_t *popped = selectDataPointer(cpu);
cpu->StackPointer++; cpu->StackPointer++;
*popped = (uint16_t)cpu->Data[cpu->StackPointer]; *popped = (uint16_t)readData(cpu, cpu->StackPointer);
cpu->StackPointer++; cpu->StackPointer++;
*popped |= (uint16_t)cpu->Data[cpu->StackPointer] << 8; *popped |= (uint16_t)readData(cpu, cpu->StackPointer) << 8;
} }
break; break;
// //
@@ -515,32 +632,32 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
break; break;
case 0x42: case 0x42:
// LDA - Load A from Data. // LDA - Load A from Data.
cpu->A = cpu->Data[*selectDataPointer(cpu)]; cpu->A = readData(cpu, *selectDataPointer(cpu));
break; break;
case 0x43: case 0x43:
// LDB - Load B from Data. // LDB - Load B from Data.
cpu->B = cpu->Data[*selectDataPointer(cpu)]; cpu->B = readData(cpu, *selectDataPointer(cpu));
break; break;
case 0x44: case 0x44:
// STQ - Store Q into Data. // STQ - Store Q into Data.
cpu->Data[*selectDataPointer(cpu)] = cpu->Q; writeData(cpu, *selectDataPointer(cpu), cpu->Q);
break; break;
case 0x45: case 0x45:
// STA - Store A into Data. // STA - Store A into Data.
cpu->Data[*selectDataPointer(cpu)] = cpu->A; writeData(cpu, *selectDataPointer(cpu), cpu->A);
break; break;
case 0x46: case 0x46:
// STB - Store B into Data. // STB - Store B into Data.
cpu->Data[*selectDataPointer(cpu)] = cpu->B; writeData(cpu, *selectDataPointer(cpu), cpu->B);
break; break;
case 0x47: { case 0x47: {
// SETD - Set the selected Data Pointer. // SETD - Set the selected Data Pointer.
uint16_t *destination = selectDataPointer(cpu); uint16_t *destination = selectDataPointer(cpu);
cpu->ProgramCounter++; cpu->ProgramCounter++;
uint16_t Address; uint16_t Address;
Address = (uint16_t)cpu->Program[cpu->ProgramCounter] << 8; // Cast the 8 bits to a 16 bit value and shift them to the high byte. Address = (uint16_t)fetchProgram(cpu, cpu->ProgramCounter) << 8; // Cast the 8 bits to a 16 bit value and shift them to the high byte.
cpu->ProgramCounter++; cpu->ProgramCounter++;
Address |= (uint16_t)cpu->Program[cpu->ProgramCounter]; Address |= (uint16_t)fetchProgram(cpu, cpu->ProgramCounter);
*destination = Address; *destination = Address;
} }
break; break;
@@ -548,14 +665,44 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// DPUP - Offset the selected Data Pointer up by the value of the next byte of Program Memory. // DPUP - Offset the selected Data Pointer up by the value of the next byte of Program Memory.
uint16_t *target = selectDataPointer(cpu); uint16_t *target = selectDataPointer(cpu);
cpu->ProgramCounter++; cpu->ProgramCounter++;
*target += cpu->Program[cpu->ProgramCounter]; *target += fetchProgram(cpu, cpu->ProgramCounter);
} }
break; break;
case 0x49: { case 0x49: {
// DPDN - Offset the selected Data Pointer down by the value of the next byte of Program Memory. // DPDN - Offset the selected Data Pointer down by the value of the next byte of Program Memory.
uint16_t *target = selectDataPointer(cpu); uint16_t *target = selectDataPointer(cpu);
cpu->ProgramCounter++; cpu->ProgramCounter++;
*target -= cpu->Program[cpu->ProgramCounter]; *target -= fetchProgram(cpu, cpu->ProgramCounter);
}
break;
case 0x4E: {
// DPUA - Offset the selected Data Pointer up by A.
//
// A rather than Q, because Q is what the ALU last worked out and would be
// gone by the time anything had been added to it. Working a step out and then
// moving a pointer by it took a store and a reload before this existed.
uint16_t *target = selectDataPointer(cpu);
*target += cpu->A;
}
break;
case 0x4F: {
// DPDA - Offset the selected Data Pointer down by A.
uint16_t *target = selectDataPointer(cpu);
*target -= cpu->A;
}
break;
case 0x50: {
// DPUW - Offset the selected Data Pointer up by A and B together, A being the
// most significant, which is how every sixteen bit value on this machine is
// carried between a pair of registers.
uint16_t *target = selectDataPointer(cpu);
*target += ((uint16_t)cpu->A << 8) | (uint16_t)cpu->B;
}
break;
case 0x51: {
// DPDW - Offset the selected Data Pointer down by A and B together.
uint16_t *target = selectDataPointer(cpu);
*target -= ((uint16_t)cpu->A << 8) | (uint16_t)cpu->B;
} }
break; break;
case 0x4A: { case 0x4A: {
@@ -566,8 +713,8 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// follows the pointer in DP0 rather than tripping over itself. // follows the pointer in DP0 rather than tripping over itself.
uint16_t *destination = selectDataPointer(cpu); uint16_t *destination = selectDataPointer(cpu);
uint16_t source = *selectDataPointer(cpu); uint16_t source = *selectDataPointer(cpu);
*destination = (uint16_t)cpu->Data[source] << 8; *destination = (uint16_t)readData(cpu, source) << 8;
*destination |= (uint16_t)cpu->Data[(uint16_t)(source + 1)]; *destination |= (uint16_t)readData(cpu, (uint16_t)(source + 1));
} }
break; break;
case 0x4B: { case 0x4B: {
@@ -576,8 +723,8 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// Data Memory when the pointer sits at the very top of it. // Data Memory when the pointer sits at the very top of it.
uint16_t value = *selectDataPointer(cpu); uint16_t value = *selectDataPointer(cpu);
uint16_t address = *selectDataPointer(cpu); uint16_t address = *selectDataPointer(cpu);
cpu->Data[address] = (value >> 8) & 0xFF; writeData(cpu, address, (value >> 8) & 0xFF);
cpu->Data[(uint16_t)(address + 1)] = value & 0xFF; writeData(cpu, (uint16_t)(address + 1), value & 0xFF);
} }
break; break;
case 0x4C: { case 0x4C: {
@@ -616,7 +763,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// OUTQ - Write the value of Q to an output port. // OUTQ - Write the value of Q to an output port.
uint16_t site = cpu->ProgramCounter; uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++; cpu->ProgramCounter++;
OutputHandler(cpu->Q, cpu->Program[cpu->ProgramCounter]); portOut(cpu, cpu->Q, fetchProgram(cpu, cpu->ProgramCounter));
answerRefusal(cpu, site); answerRefusal(cpu, site);
} }
break; break;
@@ -624,7 +771,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// OUTA - Write the value of A to an output port. // OUTA - Write the value of A to an output port.
uint16_t site = cpu->ProgramCounter; uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++; cpu->ProgramCounter++;
OutputHandler(cpu->A, cpu->Program[cpu->ProgramCounter]); portOut(cpu, cpu->A, fetchProgram(cpu, cpu->ProgramCounter));
answerRefusal(cpu, site); answerRefusal(cpu, site);
} }
break; break;
@@ -632,7 +779,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// OUTB - Write the value of B to an output port. // OUTB - Write the value of B to an output port.
uint16_t site = cpu->ProgramCounter; uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++; cpu->ProgramCounter++;
OutputHandler(cpu->B, cpu->Program[cpu->ProgramCounter]); portOut(cpu, cpu->B, fetchProgram(cpu, cpu->ProgramCounter));
answerRefusal(cpu, site); answerRefusal(cpu, site);
} }
break; break;
@@ -643,7 +790,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// INA - Read an Input to A. // INA - Read an Input to A.
uint16_t site = cpu->ProgramCounter; uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++; cpu->ProgramCounter++;
cpu->A = InputHandler(cpu->Program[cpu->ProgramCounter]); cpu->A = portIn(cpu, fetchProgram(cpu, cpu->ProgramCounter));
answerRefusal(cpu, site); answerRefusal(cpu, site);
} }
break; break;
@@ -651,7 +798,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// INB - Read an Input to B. // INB - Read an Input to B.
uint16_t site = cpu->ProgramCounter; uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++; cpu->ProgramCounter++;
cpu->B = InputHandler(cpu->Program[cpu->ProgramCounter]); cpu->B = portIn(cpu, fetchProgram(cpu, cpu->ProgramCounter));
answerRefusal(cpu, site); answerRefusal(cpu, site);
} }
break; break;
@@ -661,6 +808,23 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
case 0xF0: case 0xF0:
// NOP - Do nothing. // NOP - Do nothing.
break; break;
case 0xFE:
// WAIT - Stop fetching until a device asks for attention.
//
// NOT A HALT. The machine is still clocked and devices still run; what stops
// is the CPU's use of the bus. HALT is how a program says it has finished and
// must stay that way, so this is a separate instruction rather than a gentler
// HALT - and its state is a field of its own rather than a Status bit, for
// the reason cpu.h gives.
//
// A LINE ALREADY UP MEANS THERE IS NOTHING TO WAIT FOR, and that is what
// makes the ordinary idiom race-free: a program tests its device, finds it
// busy, and waits. If the device finished in between, the line is standing
// and this does nothing at all rather than sleeping through the answer.
if (nextPendingInterrupt() < 0) {
cpu->Waiting = 1;
}
break;
case 0xFF: case 0xFF:
// HALT - Set the Halt Bit of the Status Register. // HALT - Set the Halt Bit of the Status Register.
cpu->Status |= STATUS_HALT; cpu->Status |= STATUS_HALT;
@@ -679,6 +843,34 @@ void stepCPU(CPURegisters *cpu) {
// willing to be interrupted about it. Masking decides when a request is answered, // willing to be interrupted about it. Masking decides when a request is answered,
// not whether the outside world is allowed to have happened. // not whether the outside world is allowed to have happened.
serviceDevices(); serviceDevices();
// ---- Stopped in a WAIT ----
//
// Nothing is fetched and nothing is executed. A clock still passes, because a
// device that takes time has to be able to reach the end of it, and it is charged
// to idle rather than to the bus: the CPU is not using memory.
//
// A LINE OF ANY KIND ENDS THE WAIT, masked or not. Masking says who answers a
// request, not whether it happened - so a program can sleep on a device it has no
// handler for and simply read its status afterwards, which is the whole reason
// this is worth having and is what the filesystem does with it.
if (cpu->Waiting) {
if (nextPendingInterrupt() < 0) {
cpu->idleCycles++;
return;
}
cpu->Waiting = 0;
// Woken while masked, so nobody is going to answer this line and take it
// down. IT HAS TO BE TAKEN DOWN HERE. Left standing it would be found by the
// next WAIT, which would return at once, and by the one after that - the
// program would spin exactly as it did before while appearing to sleep.
//
// Unmasked, the dispatch below takes it down instead, and a line with no
// handler faults there the way it always has. Waiting changes what the CPU
// does between instructions; it does not change interrupt policy.
if (!(cpu->Status & STATUS_INTERRUPT)) {
clearInterrupt((uint8_t)nextPendingInterrupt());
}
}
// A device asking for attention is answered between instructions and never // A device asking for attention is answered between instructions and never
// inside one, so the address that goes into the frame is always the start of an // inside one, so the address that goes into the frame is always the start of an
// instruction and RETI always lands somewhere meaningful. // instruction and RETI always lands somewhere meaningful.
@@ -704,7 +896,7 @@ void stepCPU(CPURegisters *cpu) {
} }
} }
// The CPU is not halted, so do a cycle. // The CPU is not halted, so do a cycle.
if (executeOperation(cpu->Program[cpu->ProgramCounter], cpu)) { if (executeOperation(fetchProgram(cpu, cpu->ProgramCounter), cpu)) {
// Nothing decodes that byte. Hand it to the fault vector, which gets the // Nothing decodes that byte. Hand it to the fault vector, which gets the
// address of the offending byte itself rather than the one after it, so // address of the offending byte itself rather than the one after it, so
// that a handler can read the byte that failed and say what it was. // that a handler can read the byte that failed and say what it was.
+26
View File
@@ -62,6 +62,32 @@ typedef struct {
uint16_t StackPointer; uint16_t StackPointer;
uint8_t *Program; uint8_t *Program;
uint8_t *Data; uint8_t *Data;
// ---- What the machine has cost so far ----
//
// ONE BUS ACCESS IS ONE CYCLE, and every access goes through it: fetching an opcode,
// fetching the bytes after it, reading or writing Data Memory, pushing or popping the
// Stack, and reaching a device port. Nothing is overlapped - no fetching the next
// instruction while this one finishes - because that is a thing hardware may or may
// not do and this is the model to design against before deciding.
//
// It replaces counting instructions. Counting instructions said an RSTA and a SETD
// cost the same, and that a CALL moving ten bytes of Stack cost what a branch costs,
// which is not true of any machine anybody could build.
unsigned long busCycles;
// ---- And what it has cost while doing nothing ----
//
// Clocks spent inside WAIT, where the CPU is stopped and the bus is idle. They are
// counted because time still has to pass - a device that takes a while has to be able
// to finish - and they are counted SEPARATELY because they are not the same thing as
// work. A machine waiting on a disk is not using memory, and charging it as though it
// were is exactly the sort of dishonest number the bus count was built to replace.
unsigned long idleCycles;
// Whether the CPU is stopped in a WAIT, which is not a Status bit and must not become
// one: the Status register rides into the interrupt frame and comes back out of it, so
// a machine interrupted while waiting would return from the handler still waiting, and
// wait again for the thing it had already been given.
uint8_t Waiting;
// Set alongside the Fault Flag, and read only by whatever reports the stop. // Set alongside the Fault Flag, and read only by whatever reports the stop.
uint8_t Fault; // A FaultCause. uint8_t Fault; // A FaultCause.
uint8_t FaultVector; // Which vector was empty, when Fault is FAULT_NO_HANDLER. uint8_t FaultVector; // Which vector was empty, when Fault is FAULT_NO_HANDLER.
+91 -10
View File
@@ -5,6 +5,9 @@
// Written by Anachronaut // Written by Anachronaut
// 10/15/2024 // 10/15/2024
#include "rom.h"
#include "bootstrap.h"
#include "../Assembler/assembly.h"
#include <stdio.h> #include <stdio.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
@@ -65,6 +68,22 @@ char *programFile = NULL;
// Memory Banks: // Memory Banks:
uint8_t Program[0x10000], Data[0x10000]; uint8_t Program[0x10000], Data[0x10000];
// How the run is reported. The idle half is mentioned only when there is one, so that
// every program written before WAIT existed prints exactly the line it always did.
//
// THE TWO ARE NOT THE SAME KIND OF TIME. A bus cycle is the machine using memory; an idle
// cycle is the machine stopped in a WAIT while a device catches up. Added together they
// are elapsed time, which is what a cycle limit measures; told apart they say whether a
// program was working or waiting.
static void reportCycles(const CPURegisters *cpu, unsigned long cycleCount) {
if (cpu->idleCycles > 0) {
printf("Execution halted after %lu cycles, %lu of them waiting.\n",
cycleCount, cpu->idleCycles);
} else {
printf("Execution halted after %lu cycles.\n", cycleCount);
}
}
int main (int argc, char *argv[]) { int main (int argc, char *argv[]) {
EmulatorOptions options; EmulatorOptions options;
uint8_t result = parseOptions(argc, argv, &options); uint8_t result = parseOptions(argc, argv, &options);
@@ -78,17 +97,32 @@ int main (int argc, char *argv[]) {
if (optind < argc) { if (optind < argc) {
programFile = argv[optind]; programFile = argv[optind];
optind++; optind++;
} else {
fprintf(stderr, "Error: No boot image specified.\n");
printHelp(argv[0]);
return 1;
} }
if (optind < argc) { if (optind < argc) {
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]); fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
return 1; return 1;
} }
if (loadFile(programFile, Program, Data)) { // ---- Where the machine's first instruction comes from ----
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile); //
// Named an image, it is placed into memory and started - which is what a debugger
// does, and is how every test here runs. That path is not a shortcut to apologise
// for: placing memory from outside is a real thing real machines allow.
//
// Named none, the machine starts the way hardware would: the ROM is shadowed into
// Program Memory and it reads the disk for the rest. There has to be a disk for that
// to mean anything, and no image and no disk is a machine with nothing to run.
if (programFile == NULL && options.disk == NULL) {
fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n");
printHelp(argv[0]);
return 1;
}
if (programFile != NULL) {
if (loadFile(programFile, Program, Data)) {
fprintf(stderr, "Error: Couldn't read file: %s\n", programFile);
return 1;
}
} else if (loadROM(bootROM, bootROMBytes, Program, Data)) {
fprintf(stderr, "Error: The boot ROM is not a boot image.\n");
return 1; return 1;
} }
if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) { if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) {
@@ -104,6 +138,7 @@ int main (int argc, char *argv[]) {
} }
CycleTimer timer; CycleTimer timer;
setDiskLatency(options.diskCycles);
cycle_timer_init(&timer, CYCLE_RATE); cycle_timer_init(&timer, CYCLE_RATE);
uint8_t limitReached = 0; uint8_t limitReached = 0;
@@ -126,9 +161,55 @@ int main (int argc, char *argv[]) {
} else { } else {
cycles = cycle_timer_tick(&timer); cycles = cycle_timer_tick(&timer);
} }
for (int i = 0; i < cycles; i++) { // ---- Spending a budget of cycles, not running a count of instructions ----
//
// An instruction costs what it touches, so a batch is finished when the cycles are
// gone rather than after so many steps. In debug mode the budget is one, and any
// instruction costs at least the fetch of its own opcode, so one step still runs.
for (long spent = 0; spent < cycles; ) {
// Both kinds of cycle, because both are time passing. A step that waits
// spends no bus at all, and a budget measured only in bus cycles would never
// be spent - the machine would sit inside one batch forever and the device it
// was waiting for would never be given a moment to finish.
unsigned long before = cpu.busCycles + cpu.idleCycles;
stepCPU(&cpu); stepCPU(&cpu);
cycleCount++; unsigned long took = (cpu.busCycles + cpu.idleCycles) - before;
spent += (long)took;
cycleCount += took;
// Time has passed, so anything waiting on it may be finished.
deviceTick(cycleCount);
// ---- Starting over ----
//
// Between instructions, which is the only place it can happen: a device cannot
// restart the machine from inside the instruction that asked for it.
//
// WHAT A RESET REPEATS IS HOW THIS MACHINE STARTED. Named an image, it is
// placed again; named none, the ROM is shadowed again and reads the disk for
// the rest. Anything else would mean a reset changed what the machine is,
// which is the one thing a reset must not do.
//
// The disk is not unplugged and its image keeps everything written to it. That
// is what warm means: the machine starts again, the world it starts into does
// not.
if (takeResetRequest()) {
// The vector table goes, and that is a deliberate departure from leaving
// memory alone. A vector points into whatever installed it, and after this
// that program is not running - so a handler left behind would aim an
// interrupt at an address belonging to something gone. It is the argument
// CosmOS already makes when it takes a program's vectors back at exit.
memset(Program + SOFTWARE_VECTOR_BASE, 0,
(size_t)(0x10000 - SOFTWARE_VECTOR_BASE));
uint8_t failed = (programFile != NULL)
? loadFile(programFile, Program, Data)
: loadROM(bootROM, bootROMBytes, Program, Data);
if (failed) {
fprintf(stderr, "Error: The machine could not be started again.\n");
return 1;
}
initializeCPU(&cpu, Program, Data);
break; // Out of this batch; the loop above carries on with a new CPU.
}
if (cpu.Status & STATUS_HALT) { if (cpu.Status & STATUS_HALT) {
// We've halted. // We've halted.
break; break;
@@ -148,7 +229,7 @@ int main (int argc, char *argv[]) {
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount); printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
} else if (cpu.Status & STATUS_FAULT) { } else if (cpu.Status & STATUS_FAULT) {
// The Program Counter is still pointing at whatever the CPU could not get past. // The Program Counter is still pointing at whatever the CPU could not get past.
printf("Execution halted after %lu cycles.\n", cycleCount); reportCycles(&cpu, cycleCount);
if (cpu.Fault == FAULT_NO_HANDLER) { if (cpu.Fault == FAULT_NO_HANDLER) {
fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n", fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n",
cpu.FaultVector, cpu.ProgramCounter); cpu.FaultVector, cpu.ProgramCounter);
@@ -164,7 +245,7 @@ int main (int argc, char *argv[]) {
} }
return 1; return 1;
} else { } else {
printf("Execution halted after %lu cycles.\n", cycleCount); reportCycles(&cpu, cycleCount);
} }
return 0; return 0;
} }
+74 -7
View File
@@ -324,6 +324,15 @@ static uint8_t consoleStatus(void) {
static uint8_t pendingInterrupts[INTERRUPT_LINE_BYTES]; static uint8_t pendingInterrupts[INTERRUPT_LINE_BYTES];
// Whether somebody has asked the machine to start over, and taking that request away.
static int resetWanted = 0;
int takeResetRequest(void) {
int wanted = resetWanted;
resetWanted = 0;
return wanted;
}
void raiseInterrupt(uint8_t port) { void raiseInterrupt(uint8_t port) {
pendingInterrupts[port >> 3] |= (uint8_t)(1u << (port & 7)); pendingInterrupts[port >> 3] |= (uint8_t)(1u << (port & 7));
} }
@@ -382,6 +391,22 @@ uint8_t refusingPort(void) {
static FILE *diskImage = NULL; static FILE *diskImage = NULL;
static uint32_t diskBlockCount = 0; static uint32_t diskBlockCount = 0;
static uint8_t diskBuffer[DISK_BLOCK_BYTES]; static uint8_t diskBuffer[DISK_BLOCK_BYTES];
// ---- A disk that takes time ----
//
// The command is checked at once, because a refusal is not work: asking for a block that
// is not there, or writing to a protected disk, fails before any head moves. What takes
// time is the transfer, so that is remembered here and done when the machine has run far
// enough - and until then the buffer holds the block BEFORE this one, which is exactly
// what a program that ignores the busy bit deserves to read.
// The machine's clock as devices see it, which the emulator advances as the CPU spends
// cycles. A device says when it will be finished in these, and is believed.
static unsigned long deviceNow = 0;
static void diskTransfer(uint8_t command);
static unsigned long diskLatency = 0;
static unsigned long diskReadyAt = 0;
static uint8_t diskPending = 0;
static uint16_t diskBlock = 0; static uint16_t diskBlock = 0;
static uint8_t diskStatus = 0; static uint8_t diskStatus = 0;
static uint8_t diskProtected = 0; static uint8_t diskProtected = 0;
@@ -458,23 +483,55 @@ static void diskCommand(uint8_t command) {
raiseInterrupt(PORT_DISK); raiseInterrupt(PORT_DISK);
return; return;
} }
size_t moved = 0; if (command != DISK_COMMAND_READ && command != DISK_COMMAND_WRITE) {
if (command == DISK_COMMAND_READ) {
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
} else if (command == DISK_COMMAND_WRITE) {
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
fflush(diskImage);
} else {
diskStatus |= DISK_STATUS_ERROR; diskStatus |= DISK_STATUS_ERROR;
raiseInterrupt(PORT_DISK); raiseInterrupt(PORT_DISK);
return; return;
} }
if (diskLatency == 0) {
diskTransfer(command);
return;
}
// It is going to take a while. Say so, and remember what to do when it is over.
diskStatus |= DISK_STATUS_BUSY;
diskPending = command;
diskReadyAt = deviceNow + diskLatency;
}
// The transfer itself, whenever it happens to happen. The seek is done here rather than at
// the command, because nothing else may touch the image in between and doing it twice is
// the same answer.
static void diskTransfer(uint8_t command) {
size_t moved = 0;
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
if (fseek(diskImage, offset, SEEK_SET) != 0) {
diskStatus |= DISK_STATUS_ERROR;
} else if (command == DISK_COMMAND_READ) {
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
} else {
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
fflush(diskImage);
}
if (moved != DISK_BLOCK_BYTES) { if (moved != DISK_BLOCK_BYTES) {
diskStatus |= DISK_STATUS_ERROR; diskStatus |= DISK_STATUS_ERROR;
} }
diskStatus &= (uint8_t)~DISK_STATUS_BUSY;
raiseInterrupt(PORT_DISK); raiseInterrupt(PORT_DISK);
} }
void setDiskLatency(unsigned long cycles) {
diskLatency = cycles;
}
void deviceTick(unsigned long now) {
deviceNow = now;
if (diskPending && now >= diskReadyAt) {
uint8_t command = diskPending;
diskPending = 0;
diskTransfer(command);
}
}
// ---- A device that brings memory ---- // ---- A device that brings memory ----
// //
// The simplest thing that owns a bank. Writing to its port fills its memory with the // The simplest thing that owns a bank. Writing to its port fills its memory with the
@@ -522,6 +579,7 @@ static const DeviceRecord deviceTable[] = {
{ PORT_CONSOLE, DEVICE_CONSOLE, 0 }, { PORT_CONSOLE, DEVICE_CONSOLE, 0 },
{ PORT_TEST, DEVICE_TEST, 0 }, { PORT_TEST, DEVICE_TEST, 0 },
{ PORT_REFUSE, DEVICE_REFUSE, 0 }, { PORT_REFUSE, DEVICE_REFUSE, 0 },
{ PORT_MACHINE, DEVICE_MACHINE, 0 },
{ PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY }, { PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY },
{ PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY }, { PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY },
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 }, { PORT_REGISTRY, DEVICE_REGISTRY, 0 },
@@ -597,6 +655,15 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break; case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break; case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
case DISK_COMMAND: diskCommand(DataByte); break; case DISK_COMMAND: diskCommand(DataByte); break;
case PORT_MACHINE:
// Asked for here and acted on between instructions, because a device cannot
// restart the machine from inside the instruction that asked: the CPU is part
// way through a step and its state is not yet anything a reset could leave
// consistently behind.
if (DataByte == MACHINE_RESET) {
resetWanted = 1;
}
break;
case PORT_MEMORY: case PORT_MEMORY:
// Fills the memory this device owns with the byte written. Nothing is // Fills the memory this device owns with the byte written. Nothing is
// reachable from here: to get at it, register it as a bank and go through // reachable from here: to get at it, register it as a bank and go through
+46 -3
View File
@@ -27,6 +27,21 @@
#define PORT_REFUSE 0x11 #define PORT_REFUSE 0x11
#define PORT_MEMORY 0x12 #define PORT_MEMORY 0x12
// ---- Starting again ----
//
// Writing 1 here asks the machine to start over: whatever put the first instruction in
// memory does it again, and the CPU begins where the boot vector points.
//
// A PORT RATHER THAN A SERVICE, because a reset has to work when the system does not.
// Something that could only be asked for through SWI would be unavailable in exactly the
// case that most wants it, and a program that owns the whole machine has no system to ask.
//
// What it does NOT do is unplug anything. The disk stays attached and its image keeps
// whatever was written to it, which is what a warm restart means: the machine starts
// again, the world it starts into does not.
#define PORT_MACHINE 0x13
#define MACHINE_RESET 0x01
// The disk answers on a block of four ports and interrupts on the first of them. A device // The disk answers on a block of four ports and interrupts on the first of them. A device
// that spans more than one port raises its line on its base, which is the rule the // that spans more than one port raises its line on its base, which is the rule the
// machine has not needed until now: the controller spans sixteen and never interrupts. // machine has not needed until now: the controller spans sixteen and never interrupts.
@@ -121,6 +136,10 @@ uint8_t consoleReadByte(void);
#define DEVICE_REGISTRY 0x01 #define DEVICE_REGISTRY 0x01
#define DEVICE_CONSOLE 0x02 #define DEVICE_CONSOLE 0x02
#define DEVICE_CONTROLLER 0x03 #define DEVICE_CONTROLLER 0x03
// The machine itself, which is what a reset is asking. In the range kept for the machine
// rather than among the peripherals, because it is not one: it is not attached to
// anything and cannot be unplugged.
#define DEVICE_MACHINE 0x04
#define DEVICE_TEST 0x10 #define DEVICE_TEST 0x10
#define DEVICE_REFUSE 0x11 #define DEVICE_REFUSE 0x11
#define DEVICE_MEMORY 0x12 #define DEVICE_MEMORY 0x12
@@ -145,9 +164,12 @@ uint8_t consoleReadByte(void);
#define DISK_COMMAND_READ 0x01 #define DISK_COMMAND_READ 0x01
#define DISK_COMMAND_WRITE 0x02 #define DISK_COMMAND_WRITE 0x02
// Set while an operation is still going. It always reads clear here, because the host // Set while an operation is still going, and it now really is set: a disk given a latency
// finishes before the next instruction does, but a machine with a slower disk would set // says busy, takes that many cycles, and finishes then. A program that does not wait gets
// it and a program that ignores it would break there. Honour it anyway. // whatever was in the buffer before, which is what the hardware would give it.
//
// It reads clear the whole time when the latency is zero, which is the default and how
// every test here has always run.
#define DISK_STATUS_BUSY 0x01 #define DISK_STATUS_BUSY 0x01
// Set when the disk cannot be written at all. Unlike the two bits above it, this is not // Set when the disk cannot be written at all. Unlike the two bits above it, this is not
// about the last operation: it is a standing property of the medium, readable before // about the last operation: it is a standing property of the medium, readable before
@@ -160,6 +182,22 @@ uint8_t consoleReadByte(void);
// so it says so rather than stopping the machine. // so it says so rather than stopping the machine.
#define DISK_STATUS_ERROR 0x02 #define DISK_STATUS_ERROR 0x02
// ---- Devices that take time ----
//
// A real device does not finish inside the instruction that asked it to. It says it is
// busy, takes as long as it takes, and is done when the machine has run that far - so the
// emulator needs somewhere to notice that time has passed. That is this: called once per
// instruction with the machine's clock, it lets any device whose moment has come finish.
//
// It is written for the disk and is not about the disk. Anything that will take time - a
// display that refreshes, a port that waits on the host - wants exactly this shape.
void deviceTick(unsigned long now);
// How many cycles a block read or write takes. Zero means the answer is there before the
// next instruction is, which is what this machine has always done and what every recorded
// test assumes.
void setDiskLatency(unsigned long cycles);
// Attaches an image, making one if it is not there. A disk is read only if the host will // Attaches an image, making one if it is not there. A disk is read only if the host will
// not let the file be written, or if writeProtect asks for it, which is the emulated // not let the file be written, or if writeProtect asks for it, which is the emulated
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach. // equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
@@ -194,6 +232,11 @@ uint8_t InputHandler(uint8_t Address);
// must cost nothing when it does not. // must cost nothing when it does not.
void serviceDevices(void); void serviceDevices(void);
// Set when something has written MACHINE_RESET, and taken by the loop that acts on it. A
// request rather than an action, because a device cannot restart the machine from inside
// the instruction that asked - the CPU is mid-step and its state is not yet consistent.
int takeResetRequest(void);
void raiseInterrupt(uint8_t port); void raiseInterrupt(uint8_t port);
void clearInterrupt(uint8_t port); void clearInterrupt(uint8_t port);
+18
View File
@@ -0,0 +1,18 @@
// rom.h
// The bytes the machine wakes up in.
//
// Stage one, built from Programs/Boot/stage1.asm by the makefile rather than kept here as
// a copy, because a copy of a program stored beside the program is a copy that goes stale.
//
// It is an ordinary boot image, and that is the point: nothing about stage one changes
// when it moves from a file into a ROM except who puts it in memory.
//
// Written by Anachronaut
#ifndef ROM_H
#define ROM_H
extern const unsigned char bootROM[];
extern const unsigned long bootROMBytes;
#endif // ROM_H
+14 -2
View File
@@ -11,13 +11,20 @@
#include "../Assembler/assembly.h" #include "../Assembler/assembly.h"
void printHelp(const char *programName) { void printHelp(const char *programName) {
printf("Usage: %s [OPTIONS] <boot image>\n", programName); printf("Usage: %s [OPTIONS] [boot image]\n", programName);
printf("\n");
printf("Named an image, it is placed into memory and started, which is what a\n");
printf("debugger does and how the test suite runs. Given only a disk, the machine\n");
printf("starts the way hardware would: the built in ROM is shadowed into Program\n");
printf("Memory, and it reads the disk for everything else.\n");
printf("\n"); printf("\n");
printf("Options:\n"); printf("Options:\n");
printf(" -d, --debug Enable debug mode.\n"); printf(" -d, --debug Enable debug mode.\n");
printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n"); printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n");
printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n"); printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n");
printf(" -D, --disk FILE Attach a disk image, making one if it is not there.\n"); printf(" -D, --disk FILE Attach a disk image, making one if it is not there.\n");
printf(" -L, --disk-cycles N How many cycles a block read or write takes. Zero, the\n");
printf(" default, finishes before the next instruction starts.\n");
printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n"); printf(" -W, --write-protect Attach the disk read only. A disk the host will not let\n");
printf(" you write is read only whether you ask for this or not.\n"); printf(" you write is read only whether you ask for this or not.\n");
printf(" -h, --help Display this help message.\n"); printf(" -h, --help Display this help message.\n");
@@ -30,6 +37,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
{"fast", no_argument, 0, 'f'}, {"fast", no_argument, 0, 'f'},
{"disk", required_argument, 0, 'D'}, {"disk", required_argument, 0, 'D'},
{"write-protect", no_argument, 0, 'W'}, {"write-protect", no_argument, 0, 'W'},
{"disk-cycles", required_argument, 0, 'L'},
{"help", no_argument, 0, 'h'}, {"help", no_argument, 0, 'h'},
{0, 0, 0, 0 } {0, 0, 0, 0 }
}; };
@@ -41,9 +49,10 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
options->cycles = 0; options->cycles = 0;
options->disk = NULL; options->disk = NULL;
options->writeProtect = 0; options->writeProtect = 0;
options->diskCycles = 0;
// Parse options // Parse options
while ((opt = getopt_long(argc, argv, "dc:fhD:W", long_options, &option_index)) != -1) { while ((opt = getopt_long(argc, argv, "dc:fhD:WL:", long_options, &option_index)) != -1) {
switch (opt) { switch (opt) {
case 'd': case 'd':
options->debug = 1; options->debug = 1;
@@ -70,6 +79,9 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
case 'W': case 'W':
options->writeProtect = 1; options->writeProtect = 1;
break; break;
case 'L':
options->diskCycles = strtoul(optarg, NULL, 0);
break;
case 'h': case 'h':
printHelp(argv[0]); printHelp(argv[0]);
return OPTIONS_HELP; return OPTIONS_HELP;
+1
View File
@@ -19,6 +19,7 @@ typedef struct {
uint8_t debug; // Step one instruction at a time, printing the registers. uint8_t debug; // Step one instruction at a time, printing the registers.
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows. uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts. unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
unsigned long diskCycles; // How long a block move takes. Zero is instant, and the default.
const char *disk; // Disk image to attach, or NULL for a machine with no disk. const char *disk; // Disk image to attach, or NULL for a machine with no disk.
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would. uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
} EmulatorOptions; } EmulatorOptions;
+859
View File
@@ -0,0 +1,859 @@
// Linter.c
// Small, deliberately conservative source linter for SplitBit assembly.
//
// This first version works one source file at a time and looks only at instructions
// which are adjacent in that file. It is not an assembler and does not expand
// includes or follow control flow. That modest boundary makes every diagnostic easy
// to explain while the useful rules, and the interface they need, are still being
// discovered.
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../Assembler/assembly.h"
#include "../Emulator/cpu.h"
#define LINE_CAPACITY 1024
#define TOKEN_CAPACITY 64
#define RULE_CAPACITY 32
// Every rule this knows, named once. The count in the clean run message comes from here,
// so a rule added without a name here is a rule the summary does not know about.
static const char *const ruleNames[] = {
"redundant-setd", "redundant-assignment", "pointer-offset", "redundant-ccf",
"known-branch", "zero-load", "dead-assignment", "q-through-stack", "self-push-pop",
"branch-to-next", "unreachable", "dead-suppression",
};
#define RULE_COUNT ((int)(sizeof(ruleNames) / sizeof(ruleNames[0])))
typedef struct {
char mnemonic[TOKEN_CAPACITY];
char spelling[TOKEN_CAPACITY];
char operand[TOKEN_CAPACITY];
long literal;
int hasLiteral;
int line;
} SourceInstruction;
typedef enum {
SOURCE_OTHER,
SOURCE_INSTRUCTION,
SOURCE_LABEL,
SOURCE_BOUNDARY
} SourceLine;
typedef struct {
int aKnown;
int bKnown;
uint8_t a;
uint8_t b;
} KnownRegisters;
typedef struct {
int known;
char base[TOKEN_CAPACITY];
uint16_t offset;
} KnownPointer;
typedef enum {
CARRY_UNKNOWN = -1,
CARRY_CLEAR = 0,
CARRY_SET = 1
} KnownCarry;
static void usage(const char *name) {
printf("Usage: %s [options] <sourcefile> [sourcefile ...]\n", name);
printf("\n");
printf("Checks SplitBit assembly source for correct but needlessly long forms.\n");
printf("Warnings do not fail the command unless --fatal-warnings is given.\n");
printf("\n");
printf(" --fatal-warnings Exit non-zero if anything is reported.\n");
printf(" --machine One tab separated line per warning and nothing else:\n");
printf(" file, line, rule, message, help.\n");
printf("\n");
printf("A line whose comment says \"splitlint: <reason>\" is not reported on, and the\n");
printf("reason is required, so that a deliberate exception says what makes it one.\n");
printf("\"splitlint[rule]: <reason>\" silences one rule and leaves the line honest\n");
printf("about the others. A marker that silences nothing is itself reported.\n");
printf("\n");
printf("Rules:");
for (int i = 0; i < RULE_COUNT; i++) {
printf("%s %s", i && i % 4 == 0 ? "\n " : "", ruleNames[i]);
}
printf("\n");
}
static void uppercase(char *text) {
for (; *text; text++) {
*text = (char)toupper((unsigned char)*text);
}
}
// A semicolon inside a string belongs to the string. Anything after one outside a
// string is a comment and must not accidentally look like an instruction.
static void removeComment(char *line) {
int inString = 0;
for (char *at = line; *at; at++) {
if (*at == '"') {
inString = !inString;
} else if (*at == ';' && !inString) {
*at = '\0';
return;
}
}
}
static int literalValue(const char *text, long *value) {
int base;
if (text[0] != '0' || (text[1] != 'x' && text[1] != 'X'
&& text[1] != 'd' && text[1] != 'D')) {
return 0;
}
base = (text[1] == 'x' || text[1] == 'X') ? 16 : 10;
char *end;
long parsed = strtol(text + 2, &end, base);
if (end == text + 2 || *end != '\0' || parsed < 0 || parsed > 255) {
return 0;
}
*value = parsed;
return 1;
}
// Classifies enough of a physical source line for local analysis. Directives, strings,
// literal data, and unknown tokens are boundaries: until the linter shares the full
// assembler frontend, it makes no control-flow claim across something it did not parse.
static SourceLine parseInstruction(char *line, int lineNumber, SourceInstruction *result) {
removeComment(line);
char *token = strtok(line, " \t\r\n");
if (!token) {
return SOURCE_OTHER;
}
if (token[0] == '#' || token[0] == '"') {
return SOURCE_BOUNDARY;
}
size_t length = strlen(token);
if (length && token[length - 1] == ':') {
memset(result, 0, sizeof(*result));
if (length - 1 < sizeof(result->spelling)) {
memcpy(result->spelling, token, length - 1);
result->spelling[length - 1] = '\0';
}
result->line = lineNumber;
return SOURCE_LABEL;
}
char mnemonic[TOKEN_CAPACITY];
if (length >= sizeof(mnemonic)) {
return SOURCE_BOUNDARY;
}
strcpy(mnemonic, token);
uppercase(mnemonic);
char *selector = strchr(mnemonic, '.');
if (selector) {
*selector = '\0';
}
if (getOpcode(mnemonic) == NOT_AN_OPCODE) {
return SOURCE_BOUNDARY;
}
memset(result, 0, sizeof(*result));
strcpy(result->mnemonic, mnemonic);
strncpy(result->spelling, token, sizeof(result->spelling) - 1);
result->line = lineNumber;
char *operand = strtok(NULL, " \t\r\n");
if (operand) {
strncpy(result->operand, operand, sizeof(result->operand) - 1);
result->hasLiteral = literalValue(operand, &result->literal);
}
return SOURCE_INSTRUCTION;
}
// ---- Saying that something is deliberate ----
//
// A line whose comment carries "splitlint: <reason>" is not reported on, and the reason is
// REQUIRED: a suppression with no explanation is a way to make a tool quiet rather than a
// way to say something. "splitlint[rule]: <reason>" silences one rule and leaves the line
// honest about every other, which matters once a line can trip more than one.
//
// Warnings are reported against a line that has already been read, sometimes an earlier
// one than the line in hand, so what is kept is the set of lines seen so far.
typedef struct {
int line;
char rule[RULE_CAPACITY]; // Empty for "every rule on this line".
int used;
} Suppression;
static Suppression *suppressions = NULL;
static int suppressedCount = 0;
static int suppressedRoom = 0;
static int suppressionsUsed = 0;
static int machineReadable = 0;
static void forgetSuppressions(void) {
free(suppressions);
suppressions = NULL;
suppressedCount = 0;
suppressedRoom = 0;
}
static int rememberSuppression(int line, const char *rule) {
if (suppressedCount == suppressedRoom) {
int room = suppressedRoom ? suppressedRoom * 2 : 16;
Suppression *grown = realloc(suppressions, (size_t)room * sizeof(Suppression));
if (!grown) {
return 0;
}
suppressions = grown;
suppressedRoom = room;
}
suppressions[suppressedCount].line = line;
suppressions[suppressedCount].used = 0;
snprintf(suppressions[suppressedCount].rule, RULE_CAPACITY, "%s", rule ? rule : "");
suppressedCount++;
return 1;
}
static int isSuppressed(int line, const char *rule) {
for (int i = 0; i < suppressedCount; i++) {
if (suppressions[i].line != line) {
continue;
}
if (suppressions[i].rule[0] != '\0'
&& strcmp(suppressions[i].rule, rule) != 0) {
continue; // Names a different rule, so this one still applies.
}
suppressions[i].used = 1;
suppressionsUsed++;
return 1;
}
return 0;
}
// A suppression that no longer suppresses anything is the thing the required reason was
// meant to prevent: an exception that outlived whatever made it necessary. Saying so is
// what keeps the count of them meaningful.
static int reportDeadSuppressions(const char *path) {
int dead = 0;
for (int i = 0; i < suppressedCount; i++) {
if (suppressions[i].used) {
continue;
}
if (machineReadable) {
printf("%s\t%d\t%s\t%s\t%s\n", path, suppressions[i].line,
"dead-suppression", "this line no longer has a warning to suppress",
"remove the splitlint comment");
} else {
printf("%s:%d: style: this line no longer has a warning to suppress"
" [dead-suppression]\n", path, suppressions[i].line);
printf(" help: remove the splitlint comment\n");
}
dead++;
}
return dead;
}
// The reason has to be there and has to say something. A bare marker is refused rather
// than honoured, because a suppression nobody explained is the one that outlives whatever
// made it necessary.
//
// Returns 1 if the line is marked, 0 if it is not, and -1 if it is marked with nothing.
// A rule name in brackets, if there is one, is copied into `rule`.
static int suppressionOn(const char *rawLine, char *rule) {
rule[0] = '\0';
const char *at = strstr(rawLine, "splitlint");
if (!at) {
return 0;
}
at += strlen("splitlint");
if (*at == '[') {
const char *close = strchr(at, ']');
if (!close || close == at + 1 || (size_t)(close - at - 1) >= RULE_CAPACITY) {
return -1;
}
memcpy(rule, at + 1, (size_t)(close - at - 1));
rule[close - at - 1] = '\0';
at = close + 1;
}
if (*at != ':') {
return 0; // "splitlint" inside ordinary prose is not a marker.
}
at++;
while (*at == ' ' || *at == '\t') {
at++;
}
return (*at == '\0' || *at == '\n' || *at == '\r') ? -1 : 1;
}
static int warning(const char *path, const char *rule, int line, const char *message,
const char *help) {
if (isSuppressed(line, rule)) {
return 0;
}
if (machineReadable) {
// One line, tab separated, so nothing downstream has to read prose. This file's
// own diagnostics were parsed with regular expressions three times in a day
// before it had a shape anything could rely on.
printf("%s\t%d\t%s\t%s\t%s\n", path, line, rule, message, help);
return 1;
}
// The rule goes in brackets at the end, the way a compiler names the flag that
// produced a warning, so that the sentence still reads as a sentence.
printf("%s:%d: style: %s [%s]\n", path, line, message, rule);
printf(" help: %s\n", help);
return 1;
}
// Assignments whose only architectural effect is replacing one operand register. INA,
// INB and POP are intentionally absent: even when their result is overwritten, removing
// them would leave a byte unread from a device or an entry unconsumed from the stack.
static char assignedRegister(const SourceInstruction *instruction) {
static const char *aWriters[] = { "RSTA", "INIA", "MVQA", "LDA" };
static const char *bWriters[] = { "RSTB", "INIB", "MVQB", "LDB" };
for (size_t i = 0; i < sizeof(aWriters) / sizeof(aWriters[0]); i++) {
if (strcmp(instruction->mnemonic, aWriters[i]) == 0) {
return 'A';
}
}
for (size_t i = 0; i < sizeof(bWriters) / sizeof(bWriters[0]); i++) {
if (strcmp(instruction->mnemonic, bWriters[i]) == 0) {
return 'B';
}
}
return '\0';
}
static int stopsFallthrough(const SourceInstruction *instruction) {
return strcmp(instruction->mnemonic, "BRI") == 0
|| strcmp(instruction->mnemonic, "BRD") == 0
|| strcmp(instruction->mnemonic, "RET") == 0
|| strcmp(instruction->mnemonic, "RRET") == 0
|| strcmp(instruction->mnemonic, "RETI") == 0
|| strcmp(instruction->mnemonic, "HALT") == 0;
}
static int isDirectBranch(const SourceInstruction *instruction) {
static const char *branches[] = {
"BRI", "BRQ", "BRA", "BRB", "BRC", "BNQ", "BNA", "BNB", "BNC"
};
for (size_t i = 0; i < sizeof(branches) / sizeof(branches[0]); i++) {
if (strcmp(instruction->mnemonic, branches[i]) == 0) {
return 1;
}
}
return 0;
}
static void forgetRegisters(KnownRegisters *known) {
memset(known, 0, sizeof(*known));
}
static const char *selectorSuffix(const SourceInstruction *instruction) {
const char *suffix = strchr(instruction->spelling, '.');
return suffix ? suffix : "";
}
static int selectedPointer(const SourceInstruction *instruction) {
const char *selector = strchr(instruction->spelling, '.');
if (!selector) {
return 0;
}
char *end;
long selected = strtol(selector + 1, &end, 10);
if (end == selector + 1 || selected < 0 || selected >= DATA_POINTERS) {
return 0;
}
return (int)selected;
}
static void forgetPointers(KnownPointer pointers[DATA_POINTERS]) {
memset(pointers, 0, sizeof(*pointers) * DATA_POINTERS);
}
static int lintKnownPointers(const char *path, const SourceInstruction *instruction,
const KnownPointer pointers[DATA_POINTERS]) {
if (strcmp(instruction->mnemonic, "SETD") != 0 || !instruction->operand[0]) {
return 0;
}
int selected = selectedPointer(instruction);
const KnownPointer *pointer = &pointers[selected];
if (!pointer->known || pointer->offset != 0
|| strcmp(pointer->base, instruction->operand) != 0) {
return 0;
}
char message[180];
snprintf(message, sizeof(message), "DP%d is already known to hold %s",
selected, instruction->operand);
return warning(path, "redundant-setd", instruction->line, message, "remove the redundant SETD");
}
static void moveKnownPointer(KnownPointer *pointer, uint16_t amount, int downward) {
if (!pointer->known) {
return;
}
pointer->offset = downward ? (uint16_t)(pointer->offset - amount)
: (uint16_t)(pointer->offset + amount);
}
static void updateKnownPointers(const SourceInstruction *instruction,
const KnownRegisters *registers,
KnownPointer pointers[DATA_POINTERS]) {
int selected = selectedPointer(instruction);
KnownPointer *pointer = &pointers[selected];
if (strcmp(instruction->mnemonic, "SETD") == 0) {
if (instruction->operand[0]) {
pointer->known = 1;
strncpy(pointer->base, instruction->operand, sizeof(pointer->base) - 1);
pointer->base[sizeof(pointer->base) - 1] = '\0';
pointer->offset = 0;
} else {
pointer->known = 0;
}
} else if (strcmp(instruction->mnemonic, "INCD") == 0) {
moveKnownPointer(pointer, 1, 0);
} else if (strcmp(instruction->mnemonic, "DECD") == 0) {
moveKnownPointer(pointer, 1, 1);
} else if (strcmp(instruction->mnemonic, "DPUP") == 0 && instruction->hasLiteral) {
moveKnownPointer(pointer, (uint16_t)instruction->literal, 0);
} else if (strcmp(instruction->mnemonic, "DPDN") == 0 && instruction->hasLiteral) {
moveKnownPointer(pointer, (uint16_t)instruction->literal, 1);
} else if (strcmp(instruction->mnemonic, "DPUA") == 0 && registers->aKnown) {
moveKnownPointer(pointer, registers->a, 0);
} else if (strcmp(instruction->mnemonic, "DPDA") == 0 && registers->aKnown) {
moveKnownPointer(pointer, registers->a, 1);
} else if (strcmp(instruction->mnemonic, "DPUW") == 0
&& registers->aKnown && registers->bKnown) {
moveKnownPointer(pointer, ((uint16_t)registers->a << 8) | registers->b, 0);
} else if (strcmp(instruction->mnemonic, "DPDW") == 0
&& registers->aKnown && registers->bKnown) {
moveKnownPointer(pointer, ((uint16_t)registers->a << 8) | registers->b, 1);
} else if (strcmp(instruction->mnemonic, "LDD") == 0
|| strcmp(instruction->mnemonic, "POPD") == 0
|| strcmp(instruction->mnemonic, "MVSD") == 0) {
pointer->known = 0;
}
// ---- A CALL ends every claim, and that is a deliberate loss of precision ----
//
// CALL really does save and restore A, B and Data Pointers 0 to 2, so a pointer set
// before one is genuinely still set after it, and this used to say so - forgetting
// only DP3, which CALL does not restore. It was right, and the advice it produced was
// not safe to take.
//
// 122 of the 178 redundant SETDs it found across the corpus were redundant ONLY
// because of that restore. Removing them is correct today and becomes a wrong-pointer
// bug the moment the callee is converted from CALL to RCAL - which is not a
// hypothetical, it is what RCAL was added to this machine for, and converting the hot
// helpers was measured at close to halving the assembler's memory traffic. Worse, the
// linter would go quiet rather than complain: it forgets everything across an RCAL, so
// it would simply stop reporting while the removals stayed removed.
//
// So a claim now ends at any call, the way a claim about carry already did. Fifty
// four recommendations that stay true are worth more than a hundred and seventy eight
// that are conditional on a change the project intends to make.
if (strcmp(instruction->mnemonic, "CALL") == 0
|| strcmp(instruction->mnemonic, "RCAL") == 0
|| strcmp(instruction->mnemonic, "SWI") == 0) {
forgetPointers(pointers);
}
}
static int lintKnownRegisters(const char *path, const SourceInstruction *instruction,
const SourceInstruction *previous,
const KnownRegisters *known) {
int warnings = 0;
char message[180];
char help[160];
char previousAssignment = assignedRegister(previous);
if (known->aKnown && previousAssignment != 'A'
&& ((strcmp(instruction->mnemonic, "RSTA") == 0 && known->a == 0)
|| (strcmp(instruction->mnemonic, "INIA") == 0
&& instruction->hasLiteral && known->a == instruction->literal))) {
snprintf(message, sizeof(message), "%s leaves A at its known value of %u",
instruction->spelling, known->a);
warnings += warning(path, "redundant-assignment", instruction->line, message, "remove the redundant assignment");
}
if (known->bKnown && previousAssignment != 'B'
&& ((strcmp(instruction->mnemonic, "RSTB") == 0 && known->b == 0)
|| (strcmp(instruction->mnemonic, "INIB") == 0
&& instruction->hasLiteral && known->b == instruction->literal))) {
snprintf(message, sizeof(message), "%s leaves B at its known value of %u",
instruction->spelling, known->b);
warnings += warning(path, "redundant-assignment", instruction->line, message, "remove the redundant assignment");
}
if (known->aKnown && (strcmp(instruction->mnemonic, "DPUA") == 0
|| strcmp(instruction->mnemonic, "DPDA") == 0)) {
if (known->a == 0) {
warnings += warning(path, "pointer-offset", instruction->line, "pointer offset is known to be zero",
"remove the pointer instruction");
} else if (known->a == 1) {
snprintf(help, sizeof(help), "use %s%s",
strcmp(instruction->mnemonic, "DPUA") == 0 ? "INCD" : "DECD",
selectorSuffix(instruction));
warnings += warning(path, "pointer-offset", instruction->line, "pointer offset is known to be one", help);
}
}
if (known->aKnown && known->bKnown
&& (strcmp(instruction->mnemonic, "DPUW") == 0
|| strcmp(instruction->mnemonic, "DPDW") == 0)) {
uint16_t word = ((uint16_t)known->a << 8) | known->b;
if (word == 0) {
warnings += warning(path, "pointer-offset", instruction->line, "word-sized pointer offset is known to be zero",
"remove the pointer instruction");
} else if (word == 1) {
snprintf(help, sizeof(help), "use %s%s",
strcmp(instruction->mnemonic, "DPUW") == 0 ? "INCD" : "DECD",
selectorSuffix(instruction));
warnings += warning(path, "pointer-offset", instruction->line, "word-sized pointer offset is known to be one", help);
}
}
return warnings;
}
static void updateKnownRegisters(const SourceInstruction *instruction,
KnownRegisters *known) {
if (strcmp(instruction->mnemonic, "SHL") == 0
|| strcmp(instruction->mnemonic, "SHR") == 0) {
if (known->aKnown && known->bKnown) {
uint16_t word = ((uint16_t)known->a << 8) | known->b;
if (strcmp(instruction->mnemonic, "SHL") == 0) {
word = (uint16_t)((word << 1) | (word >> 15));
} else {
word = (uint16_t)((word >> 1) | (word << 15));
}
known->a = (uint8_t)(word >> 8);
known->b = (uint8_t)word;
} else {
known->aKnown = 0;
known->bKnown = 0;
}
return;
}
if (strcmp(instruction->mnemonic, "RSTA") == 0) {
known->aKnown = 1;
known->a = 0;
} else if (strcmp(instruction->mnemonic, "INIA") == 0 && instruction->hasLiteral) {
known->aKnown = 1;
known->a = (uint8_t)instruction->literal;
} else if (strcmp(instruction->mnemonic, "INCA") == 0) {
if (known->aKnown) known->a++;
} else if (strcmp(instruction->mnemonic, "DECA") == 0) {
if (known->aKnown) known->a--;
} else if (strcmp(instruction->mnemonic, "MVQA") == 0
|| strcmp(instruction->mnemonic, "LDA") == 0
|| strcmp(instruction->mnemonic, "POPA") == 0
|| strcmp(instruction->mnemonic, "INA") == 0) {
known->aKnown = 0;
}
if (strcmp(instruction->mnemonic, "RSTB") == 0) {
known->bKnown = 1;
known->b = 0;
} else if (strcmp(instruction->mnemonic, "INIB") == 0 && instruction->hasLiteral) {
known->bKnown = 1;
known->b = (uint8_t)instruction->literal;
} else if (strcmp(instruction->mnemonic, "INCB") == 0) {
if (known->bKnown) known->b++;
} else if (strcmp(instruction->mnemonic, "DECB") == 0) {
if (known->bKnown) known->b--;
} else if (strcmp(instruction->mnemonic, "MVQB") == 0
|| strcmp(instruction->mnemonic, "LDB") == 0
|| strcmp(instruction->mnemonic, "POPB") == 0
|| strcmp(instruction->mnemonic, "INB") == 0) {
known->bKnown = 0;
}
// A call ends what is known about A and B for the same reason it ends what is known
// about a pointer: CALL's convention saves them, so the knowledge is real, but it is
// knowledge about the CALLEE rather than about the code in hand - and it stops being
// true the day that callee is reached with RCAL instead. See updateKnownPointers.
if (strcmp(instruction->mnemonic, "CALL") == 0
|| strcmp(instruction->mnemonic, "RCAL") == 0
|| strcmp(instruction->mnemonic, "SWI") == 0) {
forgetRegisters(known);
}
}
static int lintKnownCarry(const char *path, const SourceInstruction *instruction,
KnownCarry carry) {
if (carry == CARRY_UNKNOWN) {
return 0;
}
if (strcmp(instruction->mnemonic, "CCF") == 0 && carry == CARRY_CLEAR) {
return warning(path, "redundant-ccf", instruction->line, "carry is already known to be clear",
"remove the redundant CCF");
}
if (strcmp(instruction->mnemonic, "BRC") == 0) {
if (carry == CARRY_SET) {
return warning(path, "known-branch", instruction->line, "BRC is always taken because carry is known set",
"use BRI");
}
return warning(path, "known-branch", instruction->line, "BRC is never taken because carry is known clear",
"remove the branch");
}
if (strcmp(instruction->mnemonic, "BNC") == 0) {
if (carry == CARRY_CLEAR) {
return warning(path, "known-branch", instruction->line, "BNC is always taken because carry is known clear",
"use BRI");
}
return warning(path, "known-branch", instruction->line, "BNC is never taken because carry is known set",
"remove the branch");
}
return 0;
}
static void updateKnownCarry(const SourceInstruction *instruction,
const KnownRegisters *registers,
KnownCarry *carry) {
if (strcmp(instruction->mnemonic, "CCF") == 0) {
*carry = CARRY_CLEAR;
} else if (strcmp(instruction->mnemonic, "INCA") == 0) {
*carry = registers->aKnown ? (registers->a == 0xFF ? CARRY_SET : CARRY_CLEAR)
: CARRY_UNKNOWN;
} else if (strcmp(instruction->mnemonic, "INCB") == 0) {
*carry = registers->bKnown ? (registers->b == 0xFF ? CARRY_SET : CARRY_CLEAR)
: CARRY_UNKNOWN;
} else if (strcmp(instruction->mnemonic, "DECA") == 0) {
*carry = registers->aKnown ? (registers->a == 0 ? CARRY_SET : CARRY_CLEAR)
: CARRY_UNKNOWN;
} else if (strcmp(instruction->mnemonic, "DECB") == 0) {
*carry = registers->bKnown ? (registers->b == 0 ? CARRY_SET : CARRY_CLEAR)
: CARRY_UNKNOWN;
} else if (strcmp(instruction->mnemonic, "ADD") == 0) {
if (registers->aKnown && registers->bKnown && *carry != CARRY_UNKNOWN) {
unsigned result = registers->a + registers->b + (unsigned)*carry;
*carry = result > 255 ? CARRY_SET : CARRY_CLEAR;
} else {
*carry = CARRY_UNKNOWN;
}
} else if (strcmp(instruction->mnemonic, "SUB") == 0) {
if (registers->aKnown && registers->bKnown && *carry != CARRY_UNKNOWN) {
int result = (int)registers->a - (int)registers->b - (int)*carry;
*carry = result < 0 ? CARRY_SET : CARRY_CLEAR;
} else {
*carry = CARRY_UNKNOWN;
}
} else if (strcmp(instruction->mnemonic, "CALL") == 0
|| strcmp(instruction->mnemonic, "RCAL") == 0
|| strcmp(instruction->mnemonic, "SWI") == 0
|| strcmp(instruction->mnemonic, "RET") == 0
|| strcmp(instruction->mnemonic, "RETI") == 0) {
*carry = CARRY_UNKNOWN;
}
}
static int lintInstruction(const char *path, const SourceInstruction *previous,
const SourceInstruction *current) {
int warnings = 0;
if (current->hasLiteral && current->literal == 0
&& strcmp(current->mnemonic, "INIA") == 0) {
warnings += warning(path, "zero-load", current->line, "loading zero into A takes two bytes", "use RSTA");
} else if (current->hasLiteral && current->literal == 0
&& strcmp(current->mnemonic, "INIB") == 0) {
warnings += warning(path, "zero-load", current->line, "loading zero into B takes two bytes", "use RSTB");
}
char previousAssignment = assignedRegister(previous);
if (previousAssignment && previousAssignment == assignedRegister(current)) {
char message[180];
snprintf(message, sizeof(message), "%s assigns %c, but %s replaces it immediately",
previous->spelling, previousAssignment, current->spelling);
warnings += warning(path, "dead-assignment", previous->line, message, "remove the first assignment");
}
// Q already has direct moves to both operand registers. The older stack transfer
// idiom does the same job, but costs a write, a read, and an extra instruction. This
// is precisely the kind of sequence the linter exists to find in code written before
// the newer instruction was available.
if (strcmp(previous->mnemonic, "PSHQ") == 0
&& strcmp(current->mnemonic, "POPA") == 0) {
warnings += warning(path, "q-through-stack", previous->line, "moving Q to A through the stack takes two instructions",
"use MVQA");
} else if (strcmp(previous->mnemonic, "PSHQ") == 0
&& strcmp(current->mnemonic, "POPB") == 0) {
warnings += warning(path, "q-through-stack", previous->line, "moving Q to B through the stack takes two instructions",
"use MVQB");
} else if (strcmp(previous->mnemonic, "PSHA") == 0
&& strcmp(current->mnemonic, "POPA") == 0) {
warnings += warning(path, "self-push-pop", previous->line, "pushing A and immediately restoring it leaves A unchanged",
"remove both instructions if the stack write is not intentional");
} else if (strcmp(previous->mnemonic, "PSHB") == 0
&& strcmp(current->mnemonic, "POPB") == 0) {
warnings += warning(path, "self-push-pop", previous->line, "pushing B and immediately restoring it leaves B unchanged",
"remove both instructions if the stack write is not intentional");
}
return warnings;
}
static int lintFile(const char *path) {
FILE *file = fopen(path, "r");
if (!file) {
fprintf(stderr, "%s: error: could not open source file\n", path);
return -1;
}
char line[LINE_CAPACITY];
int lineNumber = 0;
int warnings = 0;
int havePrevious = 0;
int fallthroughStopped = 0;
int havePendingBranch = 0;
SourceInstruction pendingBranch;
KnownRegisters known = {0};
KnownPointer pointers[DATA_POINTERS] = {0};
KnownCarry carry = CARRY_UNKNOWN;
SourceInstruction previous;
forgetSuppressions();
while (fgets(line, sizeof(line), file)) {
lineNumber++;
// Refuse to silently analyze only the first part of an unusually long line.
if (!strchr(line, '\n') && !feof(file)) {
fprintf(stderr, "%s:%d: error: line is longer than %d characters\n",
path, lineNumber, LINE_CAPACITY - 2);
fclose(file);
return -1;
}
// BEFORE THE COMMENT IS STRIPPED, because the marker lives in one. Recorded for
// the line rather than acted on here: a warning is reported against a line that
// has already been read, and not always the one in hand.
char suppressedRule[RULE_CAPACITY];
int marked = suppressionOn(line, suppressedRule);
if (marked < 0) {
fprintf(stderr, "%s:%d: error: splitlint: needs a reason after it\n",
path, lineNumber);
fclose(file);
forgetSuppressions();
return -1;
}
if (marked && !rememberSuppression(lineNumber, suppressedRule)) {
fprintf(stderr, "%s: error: out of memory recording a suppression\n", path);
fclose(file);
forgetSuppressions();
return -1;
}
SourceInstruction current;
SourceLine sourceLine = parseInstruction(line, lineNumber, &current);
if (sourceLine != SOURCE_INSTRUCTION) {
havePrevious = 0;
if (sourceLine == SOURCE_LABEL) {
if (havePendingBranch && current.spelling[0]
&& strcmp(pendingBranch.operand, current.spelling) == 0) {
warnings += warning(path, "branch-to-next", pendingBranch.line,
"branch target is the next labeled address",
"remove the branch");
havePendingBranch = 0;
}
fallthroughStopped = 0;
forgetRegisters(&known);
forgetPointers(pointers);
carry = CARRY_UNKNOWN;
} else if (sourceLine == SOURCE_BOUNDARY) {
fallthroughStopped = 0;
havePendingBranch = 0;
forgetRegisters(&known);
forgetPointers(pointers);
carry = CARRY_UNKNOWN;
}
continue;
}
// Reaching another instruction before the target label makes this something
// other than a branch to the next address.
havePendingBranch = 0;
if (fallthroughStopped) {
warnings += warning(path, "unreachable", current.line,
"no ordinary fallthrough reaches this instruction",
"remove it, or give an intentional indirect entry point a label");
}
SourceInstruction empty = {0};
warnings += lintKnownRegisters(path, &current,
havePrevious ? &previous : &empty, &known);
warnings += lintKnownPointers(path, &current, pointers);
warnings += lintKnownCarry(path, &current, carry);
warnings += lintInstruction(path, havePrevious ? &previous : &empty, &current);
if (stopsFallthrough(&current)) {
fallthroughStopped = 1;
}
if (isDirectBranch(&current) && current.operand[0]) {
pendingBranch = current;
havePendingBranch = 1;
}
updateKnownPointers(&current, &known, pointers);
updateKnownCarry(&current, &known, &carry);
updateKnownRegisters(&current, &known);
previous = current;
havePrevious = 1;
}
if (ferror(file)) {
fprintf(stderr, "%s: error: could not read source file\n", path);
fclose(file);
forgetSuppressions();
return -1;
}
fclose(file);
warnings += reportDeadSuppressions(path);
forgetSuppressions();
return warnings;
}
int main(int argc, char **argv) {
int fatalWarnings = 0;
int firstFile = 1;
while (firstFile < argc && argv[firstFile][0] == '-' && argv[firstFile][1] == '-') {
if (strcmp(argv[firstFile], "--fatal-warnings") == 0) {
fatalWarnings = 1;
} else if (strcmp(argv[firstFile], "--machine") == 0) {
machineReadable = 1;
} else if (strcmp(argv[firstFile], "--help") == 0) {
usage(argv[0]);
return 0;
} else {
fprintf(stderr, "%s: error: unknown option \"%s\"\n", argv[0], argv[firstFile]);
return 2;
}
firstFile++;
}
if (argc <= firstFile || strcmp(argv[firstFile], "-h") == 0) {
usage(argv[0]);
return argc <= firstFile ? 1 : 0;
}
int warnings = 0;
int files = 0;
for (int i = firstFile; i < argc; i++) {
int found = lintFile(argv[i]);
if (found < 0) {
return 2;
}
warnings += found;
files++;
}
// Machine readable output is warnings and nothing else, so that a reader can take
// every line the same way rather than having to know which ones are prose.
if (!machineReadable) {
if (warnings) {
printf("%d style warning%s found.\n", warnings, warnings == 1 ? "" : "s");
} else {
// SAYING SO IS THE POINT. A tool that exits silently has not told you it
// found nothing, it has told you nothing at all - and the two look identical
// from outside. This says what it looked at and what it looked for.
printf("No style warnings: %d file%s checked against %d rule%s.\n",
files, files == 1 ? "" : "s",
RULE_COUNT, RULE_COUNT == 1 ? "" : "s");
}
// Said out loud rather than left implicit. A suppression is a claim that something
// is deliberate, and a count of them is how anybody notices the claim has spread.
if (suppressionsUsed) {
printf("%d warning%s suppressed by splitlint comments.\n",
suppressionsUsed, suppressionsUsed == 1 ? "" : "s");
}
}
return fatalWarnings && warnings ? 1 : 0;
}

Some files were not shown because too many files have changed in this diff Show More