Compare commits

..
11 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 7e8639489b Take the direction out of the last cross references
Includes two edits of Anachronaut's: dropping a "See the Assembler Manual"
from text that is now IN the Assembler Manual, and tightening a sentence
about when a vector number has to be agreed.

The first of those was mine to have caught. Moving "Loading A Program From
A Disk" into that manual turned a cross reference into a document telling
you to go and read itself. I checked for dangling references before the
move and found none, because I checked for references by SECTION NAME and
not for references to the manuals themselves.

Checked properly now, both directions: no other document refers to itself,
and nothing that moved says "above" or "below" about something that ended up
in a different file.

The three remaining directional references are gone with them - two that
were correct today and one, "See Numbers You Write Down below", pointing at
a section twenty four lines further down that nothing guaranteed would stay
there. A reference that carries a direction is a reference that goes wrong
the next time anything moves, and this week moved a great deal.

There are none left anywhere in the repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 15:37:08 -04:00
AnachronautandClaude Opus 5 f4bc587d0a The C assembler names its output for the format, not always .bin
It always wrote .bin, whatever it had built. So assembling a loadable
program without -o produced Say.bin containing SBEX - a boot image name on a
file the machine cannot boot, in a repository whose whole convention is that
a .bin is started from and a .sbx is loaded.

    Successfully wrote SplitBit boot image to "hello.bin".
    Successfully wrote SplitBit loadable program to "Say.sbx".

programIsLoadable() already existed and is already what decides which
writer runs; the name now asks it too. Nothing in the build depended on the
old behaviour, because everything that assembles anything passes -o.

THE ASSEMBLER THAT RUNS ON SPLITBIT ALREADY DID IT THIS WAY. Two assemblers
naming their output differently from the same source is exactly the kind of
difference that wastes an afternoon, and the newer one was right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 14:54:51 -04:00
AnachronautandClaude Opus 5 b6004bdcde Say "boot image" where that is what is meant
"Binary" was doing three jobs. It meant an SPBT file that the machine starts
from; it meant whatever the assembler happened to produce, which is now
either that or a loadable program; and it meant a compiled host tool. A word
that means three things means none of them, and the first of the three has a
name already - this project has been calling them boot images for a while
and the manuals had not caught up.

  Where it means an SPBT file       -> boot image
  Where it means either output      -> output
  Where it means a host executable  -> left alone
  Where it means base two           -> left alone

The user facing messages move with it:

  Error: No boot image specified.
  Usage: ./SplitBit [OPTIONS] <boot image>
  Error: This is not a SplitBit boot image.
  Error: This boot image is in format version 2, and this emulator reads 1.
  Successfully wrote SplitBit boot image to "hello.bin".

The assembler's own help was the interesting case. Its -o writes either
format, so "the binary" there was never right - it is "the output" now, and
the message that names the format is the one that says which it wrote.

No recorded output contained the word, so nothing needed re-blessing.
Checked before starting rather than after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 14:50:03 -04:00
AnachronautandClaude Opus 5 306b4dce92 Give the Programming Manual a title, and send the boot image format away
Last of the four. What was left after the reorder was a document whose first
heading was "General Description" doing a part title's job without being
one, and a section called "Input and Output In the Emulator" that held two
console ports, a worked program, and a file format.

  A title and an opening that says what this document is FOR, and what the
  other two are for, so a reader who wants the operating system or the
  language knows immediately they are in the wrong file.

  "General Description" is "The Machine", which matches the three part
  headings the reorder gave the rest.

  "Input and Output In the Emulator" is "Making It Print Something", which
  is what the section is: port 0, and the shortest program that uses it.

THE BOOT IMAGE FORMAT MOVES TO THE ASSEMBLER MANUAL, beside the loadable
program format, for the reason SBEX went there: it is a thing the assembler
WRITES. It is fair that the emulator reads them too - both tools speak it,
the way SplitDisk and sbfs.asm both speak the filesystem - but only one of
them makes one.

And it is called a boot image now, in the text as well as the heading. That
is what this project has been calling these files for a while; the manual
was still saying "binary", which now means either kind of output file and so
means neither.

A CHECK THAT GOT BETTER BY BEING SPLIT. The hello world program and the hex
dump of it were both in the Programming Manual, and docs.sh compared them
with each other and with the assembler. The program stays with the machine,
where the reorder put it just after the instruction list; the dump goes with
the format it demonstrates. So the check now settles THREE things against
each other: what one manual prints, what the other prints, and what the
assembler actually makes. Verified both ways - a wrong byte in the dump, and
the anchor renamed.

The manual is 692 lines and four parts. It was 1,116 lines and nineteen flat
sections when this started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 14:44:11 -04:00
AnachronautandClaude Opus 5 e594f44cce Reorder the Programming Manual so it can be read from the front
The order was the order things were written in. A newcomer read the register
list and the very next heading was the vector table - an interrupt
mechanism, before a single instruction had been shown. The list of
instructions was section eighteen of nineteen, and how to make the machine
print something was dead last.

Four parts now, and each answers a question the one before it raises:

  General Description        the registers, the memories, the flags
    Naming a Data Pointer
    List of Instructions     was 18th, and nothing before it could be read
                             without it

  Making It Do Something
    Input and Output         was 19th and last. It carries the hello world
                             program, which now arrives on the heels of the
                             instruction list rather than after everything
    The Console
    The Stack Pointer, Set By Hand

  When Something Else Wants Attention
    Interrupts               the mechanism before the table, which was the
    The Vector Table         other way round and made no sense that way
    Hardware Interrupts
    Faults
    Refusing

  What A Machine Is Made Of
    Devices
    Asking What Is There
    The Memory Controller
    Storage

Nothing is rewritten. Every section is the text it was, in a different
place, so the diff is a move and can be read as one.

CROSS REFERENCES NO LONGER SAY WHICH WAY TO LOOK. "See The Console below"
was true until this commit and false after it, and three of them flipped at
once. The name is enough to find a section with, and a reference that
carries a direction is a reference that goes wrong the next time anything
moves. There will be a next time.

Left for the last commit of the four: "General Description" is doing the
work of a part title without being one, and "Input and Output In the
Emulator" now holds a worked example and a file format as well as two
ports, so it wants a better name or a split.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 14:39:56 -04:00
AnachronautandClaude Opus 5 fa3982dbd9 Move the CosmOS third of the Programming Manual to CosmOS
386 of the manual's 1,116 lines documented an operating system rather than
a machine. The split inside that file was never tutorial against reference;
it was the machine against the software that happens to run on it.

  What A Program May Ask The System For   129  -> CosmOS README
  Programs That Come With The System       111  -> CosmOS README
  Reading And Writing The Filesystem        64  -> CosmOS README
  Loading A Program From A Disk             52  -> Assembler Manual
  The Console Library                       25  -> CosmOS README

The services are the clearest case: a hundred and thirty lines describing
what CosmOS offers a program, in the manual for a CPU that has no operating
system of its own. A different system on the same machine would offer
different services and that section would be wrong for it.

The loadable program format goes to the Assembler Manual instead, because
SBEX is a thing the assembler WRITES. Nothing in the CPU knows what it is.

The Programming Manual is 716 lines and fourteen sections now, all of them
about the machine.

TWO DUPLICATE DESCRIPTIONS COLLAPSED INTO ONE EACH. The application list
existed in both documents in different words, and the CosmOS copy had gone
stale - no Break, no Stream, no assembler - because only the manual's copy
was checked. Moving the checked one in and deleting the other leaves one
list, and docs.sh follows it.

The second was made by this commit and caught while reading the seams: the
CosmOS README already had a service table, so the move briefly produced two.
That section now says what services are for and points at the one table.

Renaming a section as it moved: "Reading And Writing The Filesystem" is
"The Filesystem Library", which says what it is and reads beside "The
Console Library".

docs.sh follows all five, and each was verified by renaming the heading in
its new home and reading the complaint. The README and the CosmOS README
both described what the other manuals cover, and both were wrong the moment
this landed; they say the division out loud now, since it is the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 14:35:18 -04:00
AnachronautandClaude Opus 5 460a687939 Make the manuals plain ASCII, and check that they stay that way
"All files must be plain ASCII, the user's tooling doesn't support Unicode"
is a standing rule of this repository. Nothing enforced it, so it drifted:
39 em dashes and an ellipsis had collected in the two manuals, every one of
them typed by something that helpfully substituted a nicer character. The
spaced em dash becomes a spaced hyphen, which is what the source comments
and both READMEs use for the same job.

Tests/docs.sh now checks every tracked file and says which line and which
character. Verified that it bites.

THE CHECK READS git ls-files NUL SEPARATED, and that is the whole reason
this went unnoticed. I ran the obvious shell version of this audit two
commits ago - a loop over $(git ls-files) - and reported the repository
clean. It splits on whitespace, so it looked for a file called "SplitBit",
failed into /dev/null, and found nothing wrong with either manual because it
never opened them. Both have spaces in their names.

A check that cannot see the files with spaces in their names is worse than
no check at all, because it answers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 14:16:54 -04:00
AnachronautandClaude Opus 5 b2945e41c4 docs.sh: say which heading went missing, rather than raising IndexError
The manual checks find what they examine by splitting the file on an exact
heading. Ten of the eleven anchors already say what they could not find -
"the Programming Manual has lost its Devices table" and so on. Two did not:
the worked hello world program and the hex dump beside it were reached with
pm.split(anchor)[1] and nothing else, so renaming either produced a Python
traceback and an IndexError.

A traceback is a worse answer than a stale manual. It says a check broke
without saying which heading moved, and it stops the rest of the run, so
whatever else was wrong stays unreported.

Both anchors are now tested before they are used, and both say which one is
missing and what that means. Verified by renaming each and reading the
message.

This is the first of four commits restructuring the Programming Manual, and
it comes first on purpose: the next three move headings around, and they
should be watched by checks that would notice.

IT ALSO CORRECTS THE PLAN. I had written that renaming a heading fails
silently, and set out to fix all eleven. Probing them one at a time showed
that was wrong - ten were already fine, and the job was one check rather
than the whole file. The claim was worth testing before acting on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 14:11:37 -04:00
AnachronautandClaude Opus 5 518be9cb17 Rewrite the README around what the system is now
It had become a geological record. The overview was the 2024 one - "load
and run binary programs created for SplitBit interactively from the command
line" - and the feature list had grown by accretion, so it opened with
Debug Mode, CLI Based and Modular Codebase and buried self-hosting at
bullet fourteen of seventeen. Three sections were called "Usage".

Now it opens with what the machine is and the one fact that makes it worth
looking at, with the transcript underneath. The features are grouped into
the machine and the software running on it, and the bullets that said
nothing are gone: "Binary File Support" and "CLI Based" describe every
emulator ever written.

The three tools have names in their headings rather than three identical
"Usage" ones, and their options are tables rather than bullet lists.

There is a map of the repository, which there was not before and which the
last two commits made worth having.

TWO THINGS IT WAS SAYING THAT WERE NO LONGER TRUE, and nothing checks the
README so nothing caught them:

  "it is still the only way to get a program onto a disk, since nothing
  running on the machine assembles anything yet" - SplitDisk has not been
  the only way for two days.

  "this is what a self-hosted assembler will stand on" - future tense about
  something that has since happened.

The Tests section also claimed five checking scripts and then listed
makedisks.sh among them, which builds fixtures rather than checking
anything. It is four checks and a runner.

Every command in it was run: assembling hello.asm and running the result,
and booting the disk to see Snake.sbx load. Three em dashes crept in and
were taken out - the whole tracked repository is plain ASCII again, which
is now verified rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 13:46:59 -04:00
AnachronautandClaude Opus 5 ccf4b384e1 Give Programs/ one rule: a directory per kind, nothing loose
Five .asm files sat at the top of Programs/ beside six directories, with
nothing to say which a new file should join - and hello.asm, which is the
native assembler's first target and named in sixteen places, looked like a
stray.

    Programs/
      Examples/     what you read to learn: hello, printHello, inputTest,
                    replCalculator, and Fibonacci, primeSieve and gameOfLife
                    as sets of their own
      Libraries/    included by name, no entry point of their own
      Loader/       loader.asm, and the loadable program it reads
      CosmOS/       the system, its applications and its assembler
      testPrograms/ what 'make test' drives

Loader/ is the one worth explaining. loader.asm is not a demonstration: it
reads a program off a disk, puts the two pieces where the header asks, and
jumps to the entry. CosmOS grew out of it and does the same thing as one of
its commands. It is kept because backward compatibility with the simplest
version of the system is a standing goal, and it was sitting loose next to
the demos as though it were one.

Programs/loadable/ was a directory holding one file called hello.asm - a
third thing of that name, and the name said nothing about why it was there.
It is Loader/loadable.asm now, beside the loader that reads it.

Every reference moved with them: the makefile's program list, twelve
manifest lines, makedisks.sh, native.sh, and four paths across the README
and both manuals. Verified by deleting both build directories and running
the whole suite from nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 13:38:47 -04:00
AnachronautandClaude Opus 5 6dbb38b209 Stop committing assembled programs, nine of which had gone stale
Thirteen build artifacts were tracked: ten .bin files, a disk image, and the
text file that had been put on it. Every one is produced by 'make' in
Programs/ from a source file that is also tracked.

NINE OF THE TEN BINARIES DISAGREED WITH THEIR OWN SOURCE, including
Programs/CosmOS/Bin/cosmos.bin - the operating system. Only hello.bin still
matched, and that by accident.

That is worse than shipping no binary at all. A repository holding both a
source file and a stale build of it has two answers to what a program does,
and the wrong one is the one that runs. Somebody reads the source, runs the
binary, and sees behaviour that is not in the code with nothing to tell them
why.

Nothing referenced any of them - not the makefiles, not the tests, not the
manuals. Checked before removing, and checked after: the suite passes, and a
clean clone with all of them gone builds both tools and assembles every
program from source alone.

.gitignore now covers *.bin, *.sbx and *.img, unanchored, because a build
output may appear anywhere a source file lives.

Programs/CosmOS/Bin/ held cosmos.bin, disk.img and a 17 byte hello.txt that
existed only to be put on that image. It was a snapshot of a demo from
before Tests/makedisks.sh built its own fixtures, and it goes with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 13:29:41 -04:00
47 changed files with 1125 additions and 1016 deletions
+13
View File
@@ -13,3 +13,16 @@
# Python leaves these beside the test scripts. # Python leaves these beside the test scripts.
__pycache__/ __pycache__/
# Assembled programs. Every one of these is built by 'make' in Programs/ from a source
# file that is tracked, so committing them only creates a second answer to the question of
# what a program does - and the second answer goes stale silently. Nine of the ten that
# used to be here disagreed with their own source, including the operating system.
#
# Not anchored, because a .bin may be built anywhere a .asm lives.
*.bin
*.sbx
*.img
# Kate leaves these beside a file it has open.
.*.kate-swp
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -3
View File
@@ -6,7 +6,8 @@
; Loading and running are separate commands in this shell, so the file to assemble is the ; Loading and running are separate commands in this shell, so the file to assemble is the
; argument to run rather than a second name after the program's. ; argument to run rather than a second name after the program's.
; ;
; Reads assembly source off the disk and writes a binary back to it, with no host involved ; Reads assembly source off the disk and writes a boot image or a loadable program back to it,
; with no host involved
; anywhere. The output has to be byte for byte what the C assembler produces from the same ; anywhere. The output has to be byte for byte what the C assembler produces from the same
; source, which is the only honest test of it and the one the suite runs. ; source, which is the only honest test of it and the one the suite runs.
; ;
@@ -2046,7 +2047,7 @@ ImgHold:
DropWalk: DropWalk:
0x00 0x00 0x00 0x00
; How big a binary this can build. Everything the assembler makes has to fit here at once, ; How big an output file this can build. Everything the assembler makes has to fit here at once,
; because a file is written in one call and there is nowhere to put half of one. CosmOS ; because a file is written in one call and there is nowhere to put half of one. CosmOS
; itself comes to 9,564 bytes. ; itself comes to 9,564 bytes.
ImgRoom: ImgRoom:
@@ -2152,7 +2153,7 @@ BasesText:
at zero, on top of whatever is there. Give both a #Base, or neither. at zero, on top of whatever is there. Give both a #Base, or neither.
" "
TooBigText: TooBigText:
"the binary would be bigger than this assembler has room to build "the output would be bigger than this assembler has room to build
" "
NoWriteText: NoWriteText:
"it would not write " "it would not write "
+1 -1
View File
@@ -17,7 +17,7 @@
; ;
; 0x8000 3072 the label index, 768 entries of four ; 0x8000 3072 the label index, 768 entries of four
; 0x8C00 8192 the label names, packed end to end ; 0x8C00 8192 the label names, packed end to end
; 0xAC00 13312 the binary being built ; 0xAC00 13312 the output file being built
; 0xE000 1792 the vector names, 64 entries of twenty eight ; 0xE000 1792 the vector names, 64 entries of twenty eight
; 0xE700 1758 the reader's stack, six levels of 293 ; 0xE700 1758 the reader's stack, six levels of 293
; 0xEE00 368 which files have been included, sixteen names of 23 ; 0xEE00 368 which files have been included, sixteen names of 23
Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
Hello, SplitBit!
+342 -54
View File
@@ -125,25 +125,119 @@ without stopping the machine.
## Included Applications: ## Included Applications:
The application disk is populated from every assembly file in `CosmOS/Apps/`. At present `Programs/CosmOS/Apps` holds what the shell can load, and the application disk is built
it includes: from every assembly file in it. Several are old programs written for the bare machine that needed five edits each to become loadable ones - the Fibonacci and sieve programs, `greet`, and `hello`. The rest were written for the system as it is now, and each of those exists to show one thing working:
- `Edit`: A line-oriented text editor that can create, load, modify, and save files from | Program | What it is for |
inside CosmOS. | --- | --- |
- `Snake`: A playable terminal game using nonblocking single-key input. | Life | Conway's Game of Life, which had to be taught to stop, since a program that never ends takes the shell with it. Polls the console between generations. |
- `Life`: A 16 by 16 Conway's Game of Life simulation that returns when it settles or | Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. |
reaches its generation limit. | 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`: An interrupt-driven console demonstration carrying its own hardware vector. | Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. |
- `Say`: Demonstrates receiving the argument supplied to `run`. | 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. |
- `Files`: Writes, reads, renames, and deletes a file using only the system's services, | Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
including no filesystem code of its own. | Edit | A line editor. |
- `greet` and `hello`: Small examples of, respectively, using CosmOS services and talking | 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. |
directly to SplitBit hardware.
- `Fib-8`, `Fib-16`, and `Fib-32`: Fibonacci demonstrations at three integer widths.
- `Sieve-8` and `Sieve-16`: Prime sieves covering the 8-bit and 16-bit ranges.
These programs are ordinary SBEX files on SBFS. They are not built into the operating ### The Monitor:
system, and the host-side `SplitDisk` tool can add or remove other files from an image.
The monitor is **part of the shell**, not a program the shell loads, and that is the whole reason it works. A loaded program occupies the one place a loaded program goes, so a monitor that was an application could never look at any other application: loading the thing you wanted to inspect would replace the thing doing the inspecting.
`monitor` turns it on and the prompt changes from `>` to `*`. It is **a mode, not a detour** - the shell's own commands still work, and the mode persists until you say otherwise:
```
> load Snake.sbx
> monitor
* d 2000
2000 47 00 11 00 SETD.0 1100
* b data
bank 01
* x 1000
* exit
>
```
**A program giving the machine back lands at the prompt it was started from**, so `g` into something, letting it run, and having it exit puts you back at `*` rather than at the shell. That falls out of the mode being a variable the prompt reads rather than a second loop: every way back to the prompt goes through one place, including `osExit`. Looking at a program and running it therefore do not interrupt each other, which is the thing a monitor is for.
`exit` leaves whatever you are in - the monitor if you are in it, the machine if you are not.
| | |
| --- | --- |
| `x [addr]` | Sixty-four bytes, as hex and as characters |
| `d [addr]` | Eight instructions, disassembled |
| `a addr` | Assemble instructions, until a line that is just a dot |
| `s addr b b ...` | Put those bytes there |
| `b program\|data\|n` | Which bank to look at |
| `g addr` | Go there |
`a` writes the assembler's own syntax: a selector rides on the mnemonic as `LDA.0` or `LDD.0.1`, and leaving one off means Data Pointer 0 exactly as it does in a source file, so nothing learned at the monitor has to be unlearned when writing a program. Case does not matter, and the whole line is refused before anything is written, so a mistyped instruction leaves no half of itself behind.
```
* b data
* s 8100 68 65 6C 6C 6F 2C 20 74 79 70 65 64 0A 00
* b program
* a 8200
8200: SETD.0 8100
8204: SWI 10
8206: SWI 12
8208: .
* g 8200
hello, typed
```
A program and its data, both entered by hand, calling a system service and returning to the prompt they were written at. Note the two banks: instructions go into Program Memory and the string into Data Memory, because that is what a Harvard machine means and the monitor will not guess for you.
**Numbers here are hexadecimal and bare.** A source file writes `0x2000` or `0d16` because it has both and must say which; the monitor has one and says so once.
**What cannot be written is a label**, and that is the whole difference between this and the assembler proper. A label is a promise to fill an address in later, and later is what a line at a time does not have. It is also why the same instruction table serves both directions here: what `a` writes, `d` reads back, and neither can drift from the other or from the assembler they were generated from.
`x` and `d` share one cursor and each leaves it past what it showed, so without an address either carries on - reading through memory is one letter at a time, and you can switch between bytes and instructions without retyping where you are. `s` deliberately does not move it.
Everything else here does something; the monitor looks at what the others did. It shows memory as hex and as characters, disassembles it, writes bytes into it, and jumps to an address - all through the memory controller, which is the only thing that can reach Program Memory.
That is why a monitor is worth more on this machine than on most. Data Memory a program can already read for itself with a Data Pointer. The half it cannot see is Program Memory, and that is the half its bugs are in.
**Its instruction table is generated from the assembler's**, by `Tests/instructiontable.py`, and checked against it by `Tests/docs.sh` - along with a second check that the lengths that table implies are the ones the manual's own Bytes column prints. Both matter for the same reason: a disassembler that disagreed about how long an instruction is would not print one line wrong, it would lose its place and print everything after it wrong. Which is what a disassembler does anyway when it starts in the middle of an instruction, and is worth seeing once so it is recognised later.
**Where to put something you typed in yourself** is a question the monitor answers, because the answer moves every time the monitor is rebuilt. `m` says where its own two segments end, and those are the first free addresses:
```
> m
code from 2000, free from 2607
data from 1000, free from 1367 up to the stack
```
Which is what makes the monitor's real trick possible - a program that no assembler ever saw:
```
> s 8000 26 48 D1 00 26 49 D1 00 26 0A D1 00 18 12
> d 8000
8000 26 48 INIA 48
8002 D1 00 OUTA 00
8004 26 49 INIA 49
...
800C 18 12 SWI 12
> g 8000
HI
```
Typed in as bytes, checked by disassembling it back, and run. It ends with `SWI osExit`, which is how it gives the machine to the shell rather than to nothing.
There are no breakpoints yet, and `g` does not come back. The machinery for both already exists and nothing has used it: SplitBit has 192 undecodable bytes, and an invalid opcode dispatches through the `BadOpcode` vector carrying **the address of the offending byte**. A breakpoint is a spare byte written over an instruction and a handler waiting for it.
### The Editor:
`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 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.
Saving goes through `sbfsSaveFile`, so a document that has grown is written somewhere else and the original is only let go of once the new one is safely down. That is the whole reason the editor was written: not because the machine needed an editor, but because every tool that produces a file needs the same four operations, and building them for one imaginary tool is how they end up wrong.
These are ordinary SBEX files on SBFS. None of them is built into the operating system, and
a disk can be filled from either side: the host tool puts files on, and so does the machine,
which assembles its own now.
## The Application Model: ## The Application Model:
@@ -167,41 +261,13 @@ boundary.
### System Services: ### System Services:
Applications include `Source/services.asm` to obtain stable names and vector numbers for Applications include `Source/services.asm` to obtain stable names and vector numbers for
the services CosmOS provides. The currently installed services are: the services CosmOS provides. Neither side ever types a number: the file both of them
include is the only place any of them is written down. What each service is and what it
answers in is set out under "What A Program May Ask The System For".
| Service | Interface | The largest application CosmOS has is the assembler in `Programs/CosmOS/Assembler/`. It
| -- | -- | travels with CosmOS rather than with the emulator, for the same reason the C assembler
| `osPrintString` | DP0 names a zero-terminated string to print. | travels with the emulator: it is part of the system it was written for.
| `osReadLine` | DP0 names a destination and B is its capacity; Q returns the line length. |
| `osArgument` | DP0 names a destination and B is its capacity; receives the text following `run`. |
| `osExit` | Abandon the application's Stack, restore the CosmOS environment, and return to the shell. |
| `osFileRead` | DP0 names a file and DP1 a destination; Q reports success and DP3 returns its length in bytes. |
| `osFileSave` | DP0 names a file, DP1 supplies its contents, and A with B give the length; Q reports success. |
| `osFileDelete` | DP0 names a file to remove; Q reports success. |
| `osFileRename` | DP0 names an existing file and DP1 its new name; Q reports success. |
| `osPrintNumber` | A with B give a number to print in decimal without leading zeroes. |
| `osBreak` | Stops the application, shows every register as it had them, waits for a key, and carries on. |
| `osFileInfo` | DP0 names a file; Q reports whether it is there and DP3 returns how many blocks it occupies. |
| `osFileBlock` | DP0 names a file, DP1 a destination, and A with B give which block; Q reports success and DP3 returns how many of the block's bytes belong to the file. |
The largest application CosmOS has is the assembler in `Programs/CosmOS/Assembler/`, which
is why the streaming services below exist: it reads source a block at a time, twice, and
keeps only its label table in between. It travels with CosmOS rather than with the emulator,
for the same reason the C assembler travels with the emulator - it is part of the system it
is written for.
`osFileInfo` and `osFileBlock` are how an application reads a file too big to hold. A whole
file arrives through `osFileRead`, which cannot help with anything above 64K, and CosmOS's
own source is above it. Neither call keeps anything open: each one names the file and says
which block it wants, so an application that stops halfway leaves nothing behind. Both
report why they failed rather than only that they did - 1 for no disk, 2 for no such file,
3 for a block past the end, and 4 for a disk that would not read - because running off the
end is how a reader learns it has finished.
The filesystem services exist so that an application need not contain a second copy of the
filesystem in order to keep a file. There is deliberately no service to mount a disk: the
system mounts one before its first prompt, and an application mounting it again was only
ever a consequence of owning a private copy of the library.
A minimal CosmOS application therefore looks like this: A minimal CosmOS application therefore looks like this:
@@ -227,6 +293,224 @@ An application may also include its own libraries or access hardware ports direc
The services are an interface offered by the system, not the only way software is allowed The services are an interface offered by the system, not the only way software is allowed
to use the computer. to use the computer.
## What A Program May Ask The System For:
A loaded program is on its own hardware and can do anything the machine can do - it is a fence, not a wall. But the things it usually wants are things the system is already doing, and asking is both shorter and the only way to reach code that was assembled separately. `CALL` needs a label, and a label has to be in the same assembly; `SWI` needs only a number both sides agree on.
Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, which both the system and the program include. Neither side ever types a number.
| Service | Does |
| --- | --- |
| osPrintString | DP0 names a string ending in a zero byte. Prints it. |
| osReadLine | DP0 names somewhere to put a line, B says how much room there is. Reads one from the console. Q comes back holding how long it was. |
| osExit | Gives the machine back. Does not return. |
| osArgument | DP0 names somewhere to put whatever followed the run command, B says how much room there is. |
| osFileRead | DP0 names a file, DP1 says where to put it. Q is zero if it read, and DP3 comes back holding how many bytes there were. |
| osFileSave | DP0 names a file, DP1 is the bytes, A and B together are how many. Q is zero if it saved, whether or not it was there before. |
| osFileDelete | DP0 names a file. Q is zero if it went. |
| 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. |
| 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. |
| 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. |
```
#Include services.asm
...
SETD.0 Message
SWI osPrintString
```
### Stopping To Look:
`SWI osBreak` is a breakpoint. It shows every register as the program had them, waits for a key, and returns as though nothing happened.
```
break at 200E
A 11 B 22 Q 00 status 00
DP0 1030 DP1 05EF DP2 039A DP3 2000 SP FFFF
press a key
```
Every value comes out of the interrupt frame rather than out of the registers, because by the time the handler runs the registers belong to the handler. The frame is what the program had and what RETI is about to give back, so what is shown is what will be resumed with. The address is two before where it resumes: the `SWI` and the vector it names.
**The Stack Pointer is the exception, because it is not in the frame** - the frame is *where* the Stack Pointer is. What the program had is fourteen bytes above the frame, that being what entering an interrupt puts down, so it is worked out rather than read. Breaking inside a subroutine shows it ten lower than breaking outside one, which is the size of a CALL frame and a quick way to see how deep you are.
The status byte is shown as a number and then as the bits that are up - `carry`, `fault`, `interrupts` - because a dump that makes you look the number up is only half a dump.
**Nothing is overwritten, and that is the whole of why it is simple.** A breakpoint poked into a running program has to replace an instruction, and putting that instruction back in order to continue is the same act as disarming the breakpoint. Firing a second time would mean stepping over the restored instruction and putting the breakpoint back behind it, and this machine has no way to step a single instruction. Two bytes of `SWI` cost a little space and fire for ever, because there was never anything to restore.
The price is that a breakpoint is part of the program. A build with breakpoints in it has different addresses from a build without - the same bargain every machine makes that has a break instruction.
### The Disk Without A Filesystem:
A program that wants a file does not need to know what a filesystem is. Before these existed it had to include the whole of `sbfs.asm` - two and a half kilobytes of a private copy of code the system already had running - and then mount a disk that was already mounted.
There is no service to mount one, and that is not an omission. The system mounts the disk before it reads its first prompt, and there is one disk with one buffer registered as one bank; a program mounting it again was only ever an artefact of owning a second copy of the library. That call disappears rather than moving.
Sizes fit the registers exactly, in both directions. A file that can be read into Data Memory is under 64K by definition, so its length is sixteen bits: coming back it is DP3, and going out it is A and B together. Neither direction needs a record in memory whose shape both sides have to agree on.
A file of 256 blocks or more is refused by `osFileRead` rather than partly read, because 64K will not fit in Data Memory and its length will not fit in the pointer that reports it. A length that lies would be worse than a file that will not open.
### Reading A File That Will Not Fit:
`osFileRead` hands over a whole file, which settles the question for anything under 64K and settles nothing above it. CosmOS's own source is above it: the sources together are a hundred kilobytes and Data Memory is sixty four. A machine that is one day going to assemble itself has to be able to read a file bigger than its memory.
So there is a second way to ask. `osFileInfo` says how big something is and `osFileBlock` hands over one block of it, and between them a program reads a file of any size through a buffer of 256 bytes.
```
SETD.0 Name
SWI osFileInfo ; DP3 is how many blocks, Q is zero if it is there.
BNQ noSuchFile
readLoop:
SETD.0 Name
SETD.1 Block
SETD.2 Index
LDA.2
INCD.2
LDB.2 ; Which block, most significant first.
SWI osFileBlock
BNQ readDone ; Three when there are no more.
... ; DP3 is how many of its bytes are the file's.
```
**There is no open and no close.** Every call names the file and says which block it wants, so nothing is held between them: a program that stops halfway leaves nothing behind, and there is no handle to run out of. The system does remember where the last file it was asked about lives, so reading four hundred blocks searches the directory once rather than four hundred times - but that is a speed and not a promise, and a caller never has to know about it.
`osFileInfo` answers in **blocks rather than bytes**, and that is forced rather than chosen. A file on a sixteen megabyte disk can be twenty four bits long and a Data Pointer holds sixteen. Blocks fit; the bytes in the last one come back from `osFileBlock` when the reader gets there.
`osFileBlock` answers a count in DP3 rather than in a register for the same kind of reason: every block but a short last one holds a whole **256** bytes, and 256 does not fit in a byte. A count that reported a full block as zero would make every reader treat the end of a file as a special case.
**These two say why when the answer is no**, which the other services do not. Everywhere else the only useful thing to do about a failure is to give up, so one value is enough. These exist to be asked questions with, and the difference between the answers is the answer:
| Q | Means |
| --- | --- |
| 0 | it worked |
| 1 | there is no disk |
| 2 | there is no file of that name |
| 3 | that block is past the end of the file |
| 4 | the disk would not read it |
Running off the end is how a reader finds out it has finished, so it gets an answer of its own rather than being reported as a disk that failed.
`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.
`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.
### 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 disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one.
| Routine | Does |
| --- | --- |
| sbfsMount | Registers the disk's buffer as bank 3, reads the superblock, and checks the disk is one of ours. Q is zero if it is. |
| sbfsFind | DP0 names a file, ending in a zero byte. Q is zero if it was found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it. |
| sbfsRead | Reads the file that was found into Data Memory at DP1. Q is zero if it worked. |
| sbfsFirst | Starts a walk through the directory. Q is zero if there is an entry, and then SbfsName holds its name and the SbfsFile fields describe it. |
| sbfsNext | Steps the walk to the next entry in use. Q is zero if there was one. |
| sbfsCreate | Makes a file. DP0 names it, and SbfsFileBlocks with SbfsFileTail say how big it is. Q is zero if it was made, and then SbfsFileStart says where it went. |
| sbfsWriteFile | Writes the file that was made, from Data Memory at DP1. |
| sbfsDelete | DP0 names a file. Frees its entry and its blocks. Q is zero if it went. |
| sbfsRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it was renamed. Refused if something already answers to the new name. |
| sbfsSaveFile | DP0 names the file, DP1 is the data, and SbfsFileBlocks with SbfsFileTail say how big it now is. Writes it whether or not it was there before, and whatever size it used to be. |
Finding a file and listing what is there are different jobs. sbfsFind searches for one name; sbfsFirst and sbfsNext walk the whole directory, stopping on each entry that is in use and stepping over the free ones. A walk keeps a directory block in SbfsBuffer between calls, so anything else that goes to the disk in the middle of one ends it: take what is wanted out of an entry before asking the disk for anything else.
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.
### 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**:
```
delete the old one
make a new one <- refused, and the old one is already gone
write it
```
A create can be refused for want of a run long enough even on a disk with plenty of free blocks, because free blocks are only useful to a contiguous file when they are next to each other. Done in that order, the first fragmented disk somebody meets eats their work. sbfsSaveFile does it the other way round:
```
make a temporary nothing is lost if there is nowhere to put it
write it
delete the original only now, once the new one is safely down
rename the temporary
```
**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.
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.
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 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.
CALL saves **A, B, and Data Pointers 0, 1 and 2**, and RET puts all five back. So a subroutine cannot return anything in any of them: whatever it puts there is undone by its own return, silently, and the caller carries on with its old values as though the subroutine had never run.
What comes back is **Q**, which is one byte, and **Data Pointer 3**, which is two. That is the whole of it, and it is why DP3 is not preserved.
The same rule catches a loop that steps a pointer inside a subroutine. The step is thrown away every time round, so the loop reads the same byte forever and the fault is a wrong answer rather than a crash.
If two bytes have to come back and DP3 is spoken for, the honest answers are to write them into Data Memory, or to do the work in the caller rather than in a routine. A short sequence written out twice is better than a subroutine that quietly does nothing.
The same rule cuts the other way, which is easier to miss. Because DP3 is not put back, **a routine you call may leave something of its own in it**. It is where a routine hands a pointer out, so it is not a safe place to leave one of your own across a call to anything that might use it. The Stack is: push it before the call and pop it after, and it will be exactly as it was.
A label may only be defined once across a program and everything it includes, so a routine in one library cannot use a name that another has already taken.
## The Console Library:
Programs/CosmOS/Source/console.asm is the console library. It replaces print.asm, which was written for a machine with one Data Pointer and no vector table, and which is still there because the programs that include it still work.
| Routine | Does |
| --- | --- |
| newLine | Prints a line feed. |
| printString | DP0 names a string ending in a zero byte. Prints it. |
| printSpaces | A holds how many spaces to print. None is a fair answer, and prints nothing. |
| printByteHex | A holds a byte. Prints it as two hexadecimal digits. |
| printWordHex | DP0 names two bytes, most significant first. Prints them as four hexadecimal digits. |
| printHexDigit | A holds a nybble. Prints the one character that stands for it. |
| printDecimalDigit | A holds a digit from zero to nine. Prints it. |
| printByteDecimal | A holds a byte. Prints it in decimal, without leading zeroes. |
| printWordDecimal | DP0 names two bytes, most significant first. Prints them in decimal, without leading zeroes. |
| readLine | DP0 names a buffer and B says how many characters it holds. Reads a line into it. Q is how long the line turned out to be. |
Two things about it are different from the old library, and both are deliberate.
There is no branch at the top. print.asm begins with a BRI to a label called start, so that a program including it arrives at its own entry point rather than falling into the library. That was the only way to do it before the Vector Table existed, and it is why print.asm cannot be assembled on its own: the label it branches to is one only the including program defines. A program including console.asm says where it begins in its own Vector Segment instead, with a Boot line, and the library assembles by itself.
Every routine names the Data Pointer it works through rather than assuming there is only one. A pointer handed in is DP0, and nothing in the library disturbs DP3.
readLine cuts a line short if it is longer than the buffer, and then reads the rest of it and throws it away, so that what is left over does not turn up as the next line. ConsoleEndOfInput is set if the console ran out instead of ending a line, and it is cleared at the start of every call, so it always describes the last line read. That is a different thing from an empty line, and a program reading until there is no more has to be able to tell the two apart.
## Source Layout: ## Source Layout:
- `Source/cosmos.asm`: Boot process, shell, loader, monitor, system services, and - `Source/cosmos.asm`: Boot process, shell, loader, monitor, system services, and
@@ -278,9 +562,13 @@ development tools on the machine.
## Additional Information: ## Additional Information:
The SplitBit Programming Manual describes the CPU, devices, memory controller, SBFS, The SplitBit Programming Manual describes the machine underneath: the CPU, the vector
SBEX format, interrupt model, and CosmOS service interface. The SplitBit Assembler Manual table and interrupt model, devices, the memory controller, the console, and storage as a
documents the assembly language, loadable-program bases, and vector declarations. block device. The SplitBit Assembler Manual documents the assembly language, the segment
bases and vector declarations, and the SBEX loadable program format.
What a program may ask CosmOS for is documented here rather than in either of those,
because the services are this system's and not the machine's.
## License: ## License:
Binary file not shown.
Binary file not shown.
+19 -15
View File
@@ -16,23 +16,27 @@ BUILD ?= build
# CosmOS owns the filesystem library and the service names, so it is a place to look too. # CosmOS owns the filesystem library and the service names, so it is a place to look too.
INCLUDES = -I Libraries -I CosmOS/Source INCLUDES = -I Libraries -I CosmOS/Source
# The programs worth building. Files in Libraries/ are left out because they have no # The programs worth building. Every one lives in a directory that says what kind it is:
# entry point of their own, and the ones in testPrograms/ are covered by 'make test' # Examples/ is what you read to learn, Loader/ is the standalone loader CosmOS grew out of,
# in the parent directory. # CosmOS/ is the system. Files in Libraries/ are left out because they have no entry point
# of their own, and the ones in testPrograms/ are covered by 'make test' in the parent
# directory.
PROGRAMS = \ PROGRAMS = \
CosmOS/Source/cosmos.asm \ CosmOS/Source/cosmos.asm \
hello.asm \ Examples/hello.asm \
printHello.asm \ Examples/printHello.asm \
inputTest.asm \ Examples/inputTest.asm \
replCalculator.asm \ Examples/replCalculator.asm \
Fibonacci/8bitFibonacci.asm \ Examples/Fibonacci/8bitFibonacci.asm \
Fibonacci/16bitFibonacci.asm \ Examples/Fibonacci/16bitFibonacci.asm \
Fibonacci/32bitFibonacci.asm \ Examples/Fibonacci/32bitFibonacci.asm \
primeSieve/8bitSieve.asm \ Examples/primeSieve/8bitSieve.asm \
primeSieve/16bitSegmentedSieve.asm \ Examples/primeSieve/16bitSegmentedSieve.asm \
primeSieve/16bitSegmentedSieveModern.asm \ Examples/primeSieve/16bitSegmentedSieveModern.asm \
gameOfLife/16x16Life.asm \ Examples/gameOfLife/16x16Life.asm \
gameOfLife/16x16LifeModern.asm Examples/gameOfLife/16x16LifeModern.asm \
Loader/loader.asm \
Loader/loadable.asm
BINARIES = $(PROGRAMS:%.asm=$(BUILD)/%.bin) BINARIES = $(PROGRAMS:%.asm=$(BUILD)/%.bin)
DEPENDENCIES = $(BINARIES:.bin=.d) DEPENDENCIES = $(BINARIES:.bin=.d)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+142 -87
View File
@@ -1,142 +1,197 @@
# SplitBit Emulator # SplitBit
## Overview: SplitBit is an 8 bit computer that does not exist: a CPU with its own instruction set, split Program and Data memories, an interrupt and vector system, a bus that programs can enumerate, a memory controller that can write code into memory, and a disk. This repository is a C implementation of the machine, an assembler for it, a tool for its disks, and the software that runs on it, which now includes an operating system and an assembler written in SplitBit's own assembly language.
SplitBit is a custom 8 bit system designed for hobbyist projects and experimentation: a CPU with its own instruction set, an interrupt and vector system, a bus that programs can enumerate, and a memory controller that can load code. The SplitBit Emulator is a C implementation of it. It allows users to load and run binary programs created for SplitBit interactively from the command line.
### Features: **SplitBit assembles SplitBit.** `Programs/CosmOS/Assembler/` runs on the machine, reads source off a SplitBit disk, and writes a boot image or a loadable program back to it with no host involved. It builds the operating system it is running under, and it builds itself, both byte for byte identical to what the C assembler produces from the same source. The test suite then boots the CosmOS that CosmOS built and has *that* one assemble CosmOS again, and the second generation is identical to the first, so the machinery has been through itself. After that the host is a convenience rather than a necessity.
- 8-bit Harvard Architecture: The system memory is separated into two 64k banks, one for the Program Memory and another for the Data Memory.
- Custom ISA: A fully implemented instruction set architecture optimized for simplicity and easy assembly programming. ```
- Debug Mode: Single-step through instructions and monitor the CPU's registers as they change through each cycle. > load Asm.sbx
- CLI Based: Debug messages and CPU input and output are supported through the command line. > run cosmos.asm
- Binary File Support: Load programs and data from binary files. wrote cosmos.bin: program 7036, data 2448, labels 475
- Modular Codebase: Mostly clean separation of CPU, I/O, and utility functions for easy modification. > run Asm.asm
- Interrupts: Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save. wrote Asm.sbx: program 7533, data 4099, labels 555
- Devices: A bus registry that says what a machine is made of, so a program can ask rather than being told. ```
- Filesystem: SBFS, read and written by SplitBit itself, and by a host tool that speaks the same format so an image can be moved either way.
- Loadable Programs: A program that was not booted from carries a header saying where it belongs, and Programs/loader.asm reads one off a disk, puts it there, and runs it. ## What Is In Here:
- An Operating System: CosmOS boots the machine, mounts a disk, lists what is on it, loads a program and runs it, and takes the machine back when it finishes. It comes with a library of programs to run, including a game and a line editor that writes files a person typed.
- System Services: A loaded program reaches the console and the disk through numbered software interrupts rather than carrying a copy of the code that drives them. The numbers are written down in one file that both sides include, so neither ever types one. It took the editor from 4941 bytes to 1983 without changing a line of what it does. | Directory | What it holds |
- A Native Assembler: SplitBit assembles SplitBit. Programs/CosmOS/Assembler/ is an assembler written in SplitBit assembly that runs under CosmOS, reads source off a SplitBit disk, and writes a binary back to it with no host involved. It builds boot images and loadable applications, following every directive the language has. IT ASSEMBLES COSMOS, AND IT ASSEMBLES ITSELF, both byte for byte identical to what the C assembler produces from the same source. Tests/native.sh then boots the CosmOS that CosmOS built and has that one assemble CosmOS again, so the machinery has been through itself: after that the host is a convenience rather than a necessity. | --- | --- |
- Streaming Reads: A file bigger than the machine's memory is read a block at a time, through services that keep nothing open between calls. CosmOS's own source is 104K against 64K of Data Memory, so this is what a self-hosted assembler will stand on. | `Source/Emulator` | The machine: CPU, memory controller, devices, console, disk |
- Storage: A block device with 256 byte blocks and 16 megabytes of them, backed by an image file on the host. It knows blocks and not files, because a filesystem is meant to be software SplitBit runs. | `Source/Assembler` | The assembler that runs on a host |
- Memory Controller: Reads and writes Program Memory, moves blocks between memory 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. | `Source/DiskTool` | SplitDisk, which reads and writes SplitBit's filesystem |
- Assembler: Assemble human readable assembly language files directly into SplitBit compatible binary files. Supports including external files, handling labels, alignment and reservation, and defining Program, Data and Vector segments. | `Programs/Examples` | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life |
| `Programs/Libraries` | Code included by name rather than linked, since there is no linker |
| `Programs/Loader` | The standalone loader CosmOS grew out of |
| `Programs/CosmOS` | The operating system, its applications, and the native assembler |
| `Programs/testPrograms` | What the test suite drives |
| `Tests` | The suite: the manifest, the recorded output, and the scripts that check it |
## 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.
- **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.
- **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 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.
- **Storage**: a block device with 256 byte blocks and up to 16 megabytes of them, backed by an image file. It knows blocks and not files, because a filesystem is meant to be software SplitBit runs.
## The Software:
- **SBFS**, a filesystem read and written by SplitBit itself and by a host tool that speaks the same format, so an image can be moved either way and each implementation checks the other.
- **CosmOS**, an operating system: it boots the machine, mounts a disk, lists what is on it, loads a program and runs it, and takes the machine back when the program finishes. It comes with a library of programs including a game, a line editor, and a monitor that examines memory, disassembles, assembles a line at a time, and sets breakpoints.
- **Loadable programs.** A program that was not booted from carries a header saying where it belongs, and may bring interrupt handlers of its own for the loader to install and take back again.
- **System services.** A loaded program reaches the console and the disk through numbered software interrupts rather than carrying a copy of the code that drives them. The numbers are written down in one file both sides include, so neither ever types one. It took the editor from 4941 bytes to 1983 without changing a line of what it does.
- **Streaming reads.** A file bigger than the machine's memory is read a block at a time, through services that keep nothing open between calls. CosmOS's own source is far larger than its 64K of Data Memory, and this is what the native assembler stands on.
## Getting Started:
Clone it and build the three tools. You need gcc and make, or similar:
### Installation:
1) Clone the repository:
``` ```
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
cd SplitBit-Emulator cd SplitBit-Emulator
```
2) Build the Emulator, the Assembler and the disk tool: You'll need gcc and make or similar.
```
make make
``` ```
The sources are ISO C, and build clean under -std=c11 -pedantic with -Wall -Wextra. Beyond ISO C they need POSIX.1-2008, which the makefile asks for by name, and getopt_long for the long form of the command line options.
3) Assemble a program:
```
./Assembler Programs/hello.asm
```
4) Run the program: The sources are ISO C and build clean under `-std=c11 -pedantic` with `-Wall -Wextra`. Beyond ISO C they need POSIX.1-2008, which the makefile asks for by name, and `getopt_long` for the long form of the command line options.
Assemble something and run it:
``` ```
./Assembler Programs/Examples/hello.asm
./SplitBit hello.bin ./SplitBit hello.bin
``` ```
### Usage: Or boot the operating system, with a disk of programs and all of its own source on it:
```
./SplitBit [options] [binary file]
```
#### Options:
- -d, --debug: Enable debug mode to single step through cycles. Each key press advances one instruction.
- -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 machine allows, ignoring the emulated cycle rate.
- -D, --disk \<file\>: Attach a disk image, creating a 128K one if the file is not there.
- -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.
#### Notes:
- 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.
### Usage:
``` ```
./SplitDisk <command> <image> [arguments] cd Programs
make run-cosmos
``` ```
#### Commands:
- 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.
- list \<image\>: Show what is on the disk.
- 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.
- get \<image\> \<name\> [file]: Take one off it.
- delete \<image\> \<name\>: Remove one.
#### Notes: Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, or `load Asm.sbx` and `run cosmos.asm` to watch the machine build itself.
- 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. SplitBit writes its own filesystem now, so this is not the only way to get something onto a disk; it is still the only way to get a program onto one, since nothing running on the machine assembles anything yet.
- 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. ## Running Programs: SplitBit
```
./SplitBit [options] [boot image]
```
| Option | What it does |
| --- | --- |
| `-d`, `--debug` | Single step through cycles. Each key press advances one instruction. |
| `-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. |
| `-D`, `--disk <file>` | Attach a disk image, creating a 128K one if the file is not there. |
| `-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. |
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
### Usage:
``` ```
./Assembler [options] [assembly file] ./Assembler [options] [assembly file]
``` ```
#### Options:
- -o \<file\>: Write the binary to this path.
- -I \<dir\>: Look in this directory for included files. May be given more than once.
- -M \<file\>: Write out which source files the binary depends on, as a make rule.
- -h, --help: Show help and usage information.
#### Notes: | Option | What it does |
- Without -o, the assembled binary is saved with the same name as the assembly source file, with a .bin extension, in the directory that you call the assembler from. | --- | --- |
- Included files are looked for beside the file that includes them, and then along the directories given with -I. | `-o <file>` | Write the output to this path. |
| `-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. |
| `-h`, `--help` | Show help and usage information. |
### Building Programs With Make: 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`.
The assembler is built to work with make. The -o option puts the binary 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.
## Managing Disks: SplitDisk
```
./SplitDisk <command> <image> [arguments]
```
| 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. |
| `list <image>` | Show what is on the disk. |
| `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. |
| `get <image> <name> [file]` | Take one off it. |
| `delete <image> <name>` | Remove one. |
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.
## 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.
`Programs/makefile` does this for the programs in this repository:
Programs/makefile does this for the programs in this repository:
``` ```
cd Programs cd Programs
make make
``` ```
The rule it uses is small enough to copy into your own projects: The rule it uses is small enough to copy into your own projects:
``` ```
$(BUILD)/%.bin: %.asm $(BUILD)/%.bin: %.asm
@mkdir -p $(@D) @mkdir -p $(@D)
$(ASM) -I Libraries -M $(@:.bin=.d) -o $@ $< $(ASM) $(INCLUDES) -M $(@:.bin=.d) -o $@ $<
-include $(BINARIES:.bin=.d) -include $(BINARIES:.bin=.d)
``` ```
### Tests: ## Tests:
The test suite assembles and runs every program in Programs/ and compares the results against recorded output.
``` ```
make test make test
``` ```
Disk images that tests read from are built by Tests/makedisks.sh before the run, using SplitDisk. A test that reads one is therefore checked against a filesystem written by different code from the same written format, rather than against itself.
The disk tool is checked separately by Tests/disk.sh, which make test runs afterwards: it puts files of every awkward size onto an image and takes them off again, and checks that the things the format says cannot happen are refused. The suite assembles and runs every program in `Programs/` and compares the results against recorded output. Tests are defined in `Tests/manifest`, one line per program. To record the current output as the expected result, after you have checked that it is correct:
Tests/terminal.sh checks the things a recorded output cannot see. Every other test pipes input in and output to a file, which answers what a program prints and is blind to two whole classes of behaviour: **when** something is printed, since 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 **what happens to the terminal**, since key mode only touches one 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 the questions 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.
A cycle count is deliberately **not** part of a recorded result. The last line of the emulator's output has the number taken out of it 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 arrive in a crowd of meaningless ones. Anything that wants to measure cycles should say so in a test of its own.
Tests/docs.sh then 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 routine the manuals promise exists, and that the worked examples still assemble to the bytes printed beside them. Documentation goes stale quietly, and this is what stops it.
Tests are defined in Tests/manifest, one line per program. To record the current output as the expected result, after you have checked that it is correct:
``` ```
make bless make bless
``` ```
Programs are built inside Tests/build, so running the suite never overwrites the binaries in Programs/. To run only some of the tests, call the runner directly with their names:
Programs are built inside `Tests/build`, so running the suite never overwrites anything in `Programs/`. To run only some of the tests, call the runner directly with their names:
``` ```
./Tests/run.sh hello 8bitFibonacci ./Tests/run.sh hello 8bitFibonacci
``` ```
The disk images tests read from are built first by `Tests/makedisks.sh`, using SplitDisk. A
test that reads one is therefore checked against a filesystem written by different code from
the same written specification, rather than against itself.
`Tests/run.sh` drives that comparison. Four more scripts run alongside it, and each exists
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/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/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.
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 both tools with the address and undefined behaviour sanitizers and run the suite under them: To rebuild both tools with the address and undefined behaviour sanitizers and run the suite under them:
``` ```
make sanitize make sanitize
``` ```
This catches reads and writes past the end of an array, use after free, leaks, and undefined arithmetic. It also fills fresh allocations with a junk pattern, which turns a read of uninitialised memory from something that quietly works into something the tests notice. It takes about twice as long as make test, and puts the ordinary binaries back when it finishes.
### Additional Info: This catches reads and writes past the end of an array, use after free, leaks, and undefined arithmetic. It also fills fresh allocations with a junk pattern, which turns a read of uninitialised memory from something that quietly works into something the tests notice. It takes about twice as long as `make test`, and puts the ordinary binaries back when it finishes.
For more information on the custom ISA and programming for SplitBit, see the Programming Manual and Assembler Manual.
### License: ## Documentation:
This project is licensed under the Apache License, Version 2.0. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
Three documents, divided by what they are about rather than by who reads them.
The **SplitBit Programming Manual** describes **the machine**: the instruction set, the registers, the vector table, interrupts, devices, the memory controller, the console, storage, and faults. Everything here is true of any SplitBit, whatever is running on it.
The **SplitBit Assembler Manual** describes **the language and the file formats**: literal values, labels, segments, the directives, the loadable program header, and the assembler that runs on SplitBit itself.
`Programs/CosmOS/README.md` describes **the operating system**: its shell, its applications, what a program may ask it for, and the libraries it owns. A different system on the same machine would answer all of that differently, which is why it is documented with the system rather than with the CPU.
## License:
Apache License, Version 2.0. You may obtain a copy at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
+18 -10
View File
@@ -20,7 +20,16 @@ int dataLength = 0;
uint8_t Program[0xFFFF], Data[0xFFFF]; uint8_t Program[0xFFFF], Data[0xFFFF];
// What an assembled file is called when nobody said. The extension follows the FORMAT
// rather than being always .bin: a boot image is what the machine starts from and a
// loadable program is what a running system loads, and this repository has called them
// .bin and .sbx apart for long enough that a .bin holding SBEX is a small lie.
//
// The assembler that runs on SplitBit already chose this way. Two assemblers naming their
// output differently from the same source is exactly the sort of difference that wastes an
// afternoon.
char* createOutputFileName(const char *inputFilePath) { char* createOutputFileName(const char *inputFilePath) {
const char *extension = programIsLoadable() ? ".sbx" : ".bin";
// Make a copy of inputFilePath, since basename may modify it // Make a copy of inputFilePath, since basename may modify it
char *pathCopy = strdup(inputFilePath); char *pathCopy = strdup(inputFilePath);
if (!pathCopy) { if (!pathCopy) {
@@ -37,26 +46,25 @@ char* createOutputFileName(const char *inputFilePath) {
// Check if the filename ends with ".asm" // Check if the filename ends with ".asm"
char *outputFileName; char *outputFileName;
if (len > 4 && strcmp(fileName + len - 4, ".asm") == 0) { if (len > 4 && strcmp(fileName + len - 4, ".asm") == 0) {
// Allocate memory for the new file name with ".bin" extension // Remove ".asm" (4 chars) and add the extension (4 chars plus a null terminator).
outputFileName = malloc(len - 4 + 5); // Remove ".asm" (4 chars) and add ".bin" (4 chars + null terminator) outputFileName = malloc(len - 4 + 5);
if (!outputFileName) { if (!outputFileName) {
fprintf(stderr, "Error: Memory allocation failed for output file name.\n"); fprintf(stderr, "Error: Memory allocation failed for output file name.\n");
free(pathCopy); free(pathCopy);
exit(1); exit(1);
} }
// Copy the filename up to ".asm" and add ".bin"
strncpy(outputFileName, fileName, len - 4); strncpy(outputFileName, fileName, len - 4);
strcpy(outputFileName + len - 4, ".bin"); strcpy(outputFileName + len - 4, extension);
} else { } else {
// If there's no ".asm" extension, add ".bin" to the full filename // No ".asm" to replace, so the extension goes on the end of the whole name.
outputFileName = malloc(len + 5); // Original length + ".bin" + null terminator outputFileName = malloc(len + 5);
if (!outputFileName) { if (!outputFileName) {
fprintf(stderr, "Error: Memory allocation failed for output file name.\n"); fprintf(stderr, "Error: Memory allocation failed for output file name.\n");
free(pathCopy); free(pathCopy);
exit(1); exit(1);
} }
strcpy(outputFileName, fileName); strcpy(outputFileName, fileName);
strcat(outputFileName, ".bin"); strcat(outputFileName, extension);
} }
free(pathCopy); // Free the temporary path copy free(pathCopy); // Free the temporary path copy
@@ -91,13 +99,13 @@ void printUsage(const char *programName) {
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName); printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
printf("\n"); printf("\n");
printf("Options:\n"); printf("Options:\n");
printf(" -o <file> Write the binary 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 binary depends on, as a make rule.\n"); printf(" -M <file> Write the source files this output depends on, as a make rule.\n");
printf(" -h, --help Display this help message.\n"); printf(" -h, --help Display this help message.\n");
} }
// Writes a make rule naming every source file that went into the binary, so that a // Writes a make rule naming every source file that went into the output, so that a
// build system knows to reassemble when any of them changes. The empty rules after it // build system knows to reassemble when any of them changes. The empty rules after it
// are so that deleting a library does not leave make with a prerequisite it cannot // are so that deleting a library does not leave make with a prerequisite it cannot
// build; without them the build stops instead of just reassembling. // build; without them the build stops instead of just reassembling.
+7 -7
View File
@@ -8,13 +8,13 @@
#ifndef ASSEMBLY_H #ifndef ASSEMBLY_H
#define ASSEMBLY_H #define ASSEMBLY_H
// ---- The SplitBit binary format ---- // ---- The SplitBit boot image format ----
// //
// A binary starts with a file header, then the Program Segment, then the Data // A boot image starts with a file header, then the Program Segment, then the Data
// Segment. All multi byte numbers are stored most significant byte first. // Segment. All multi byte numbers are stored most significant byte first.
// //
// Offset Size Field // Offset Size Field
// 0 4 "SPBT", so a file that is not a SplitBit binary is spotted at once // 0 4 "SPBT", so a file that is not a boot image is spotted at once
// 4 1 Format version // 4 1 Format version
// 5 4 Required feature flags // 5 4 Required feature flags
// 9 3 "PRG" // 9 3 "PRG"
@@ -27,14 +27,14 @@
// .. 2 Vector Segment length, in bytes // .. 2 Vector Segment length, in bytes
// .. K Vector Segment, four bytes per entry // .. K Vector Segment, four bytes per entry
// //
// The Vector Segment is optional and comes last, so a binary written before it existed // The Vector Segment is optional and comes last, so an image written before it existed
// simply ends after its Data Segment and still loads. Each entry is two bytes saying // simply ends after its Data Segment and still loads. Each entry is two bytes saying
// where in Program Memory the vector sits, then two bytes saying where its handler is, // where in Program Memory the vector sits, then two bytes saying where its handler is,
// most significant byte first. It is a list rather than an image of the table, so a // most significant byte first. It is a list rather than an image of the table, so a
// program with three handlers costs twelve bytes instead of a padded kilobyte. // program with three handlers costs twelve bytes instead of a padded kilobyte.
// //
// The feature flags are how a binary says it needs something the base machine does // The feature flags are how a boot image says it needs something the base machine does
// not provide, so that an emulator which cannot provide it refuses to run the binary // not provide, so that an emulator which cannot provide it refuses to run the image
// rather than quietly doing the wrong thing. No features are defined yet; the field // rather than quietly doing the wrong thing. No features are defined yet; the field
// is here so that adding one later does not need another format version. // is here so that adding one later does not need another format version.
@@ -110,7 +110,7 @@
#define SPLITBIT_HEADER_BYTES (SPLITBIT_MAGIC_LENGTH + 1 + SPLITBIT_FLAGS_LENGTH \ #define SPLITBIT_HEADER_BYTES (SPLITBIT_MAGIC_LENGTH + 1 + SPLITBIT_FLAGS_LENGTH \
+ 2 * (SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES)) + 2 * (SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES))
// Features this build of the emulator can provide. A binary asking for anything // Features this build of the emulator can provide. An image asking for anything
// outside this set is refused. // outside this set is refused.
#define SPLITBIT_FEATURES_SUPPORTED 0x00000000u #define SPLITBIT_FEATURES_SUPPORTED 0x00000000u
+3 -3
View File
@@ -653,7 +653,7 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
} }
// Write the file header: the magic, the format version, and the features this // Write the file header: the magic, the format version, and the features this
// binary needs from the machine. An emulator that cannot provide one of those // boot image needs from the machine. An emulator that cannot provide one of those
// features refuses the file rather than running it and going quietly wrong. // features refuses the file rather than running it and going quietly wrong.
fwrite(SPLITBIT_MAGIC, sizeof(char), SPLITBIT_MAGIC_LENGTH, outputFile); fwrite(SPLITBIT_MAGIC, sizeof(char), SPLITBIT_MAGIC_LENGTH, outputFile);
fputc(SPLITBIT_FORMAT_VERSION, outputFile); fputc(SPLITBIT_FORMAT_VERSION, outputFile);
@@ -703,7 +703,7 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
} }
// The Vector Segment, only if the program named any. Leaving it out entirely is // The Vector Segment, only if the program named any. Leaving it out entirely is
// what lets a binary written before vectors existed still load: the reader treats // what lets an image written before vectors existed still load: the reader treats
// the end of the file as an empty table rather than a missing one. // the end of the file as an empty table rather than a missing one.
int installed = 0; int installed = 0;
for (int i = 0; i < vectorArrayCount; i++) { for (int i = 0; i < vectorArrayCount; i++) {
@@ -730,7 +730,7 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
} }
fclose(outputFile); fclose(outputFile);
printf("Successfully wrote SplitBit binary to \"%s\".\n", outputFileName); printf("Successfully wrote SplitBit boot image to \"%s\".\n", outputFileName);
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n" RESET, programCount, dataCount); printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n" RESET, programCount, dataCount);
if (installed > 0) { if (installed > 0) {
printf(GREEN " Vectors: %d.\n" RESET, installed); printf(GREEN " Vectors: %d.\n" RESET, installed);
+1 -1
View File
@@ -8,7 +8,7 @@
// has to change there in the same breath. // has to change there in the same breath.
// //
// All multi byte numbers are most significant byte first, the same as every other number // All multi byte numbers are most significant byte first, the same as every other number
// SplitBit stores: addresses, the SPBT binary header, and the vector table. // SplitBit stores: addresses, the SPBT boot image header, and the vector table.
// //
// Written by Anachronaut // Written by Anachronaut
+7 -7
View File
@@ -4,7 +4,7 @@
// 10/16/2024 // 10/16/2024
#include "bootstrap.h" #include "bootstrap.h"
#include "../Assembler/assembly.h" // For the binary 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 <string.h> #include <string.h>
@@ -39,14 +39,14 @@ static uint8_t readMarker(FILE *file, const char *expected, int length, char *fo
return strncmp(found, expected, length) != 0; return strncmp(found, expected, length) != 0;
} }
// Reads the file header: the magic, the format version, and the features the binary // Reads the file header: the magic, the format version, and the features the boot image
// says it needs from the machine. // says it needs from the machine.
static uint8_t readFileHeader(FILE *file) { static uint8_t readFileHeader(FILE *file) {
char magic[SPLITBIT_MAGIC_LENGTH + 1]; char magic[SPLITBIT_MAGIC_LENGTH + 1];
if (readMarker(file, SPLITBIT_MAGIC, SPLITBIT_MAGIC_LENGTH, magic)) { if (readMarker(file, SPLITBIT_MAGIC, SPLITBIT_MAGIC_LENGTH, magic)) {
fprintf(stderr, "Error: This is not a SplitBit binary.\n"); fprintf(stderr, "Error: This is not a SplitBit boot image.\n");
if (strncmp(magic, "PRG", 3) == 0) { if (strncmp(magic, "PRG", 3) == 0) {
fprintf(stderr, " It looks like a binary from before the format carried a version.\n Reassemble it and try again.\n"); fprintf(stderr, " It looks like a boot image from before the format carried a version.\n Reassemble it and try again.\n");
} else { } else {
fprintf(stderr, " Expected the file to begin with \"%s\", found \"%s\".\n", SPLITBIT_MAGIC, magic); fprintf(stderr, " Expected the file to begin with \"%s\", found \"%s\".\n", SPLITBIT_MAGIC, magic);
} }
@@ -58,7 +58,7 @@ static uint8_t readFileHeader(FILE *file) {
return 1; return 1;
} }
if (version != SPLITBIT_FORMAT_VERSION) { if (version != SPLITBIT_FORMAT_VERSION) {
fprintf(stderr, "Error: This binary is in format version %u, and this emulator reads version %u.\n", version, SPLITBIT_FORMAT_VERSION); fprintf(stderr, "Error: This boot image is in format version %u, and this emulator reads version %u.\n", version, SPLITBIT_FORMAT_VERSION);
return 1; return 1;
} }
@@ -68,7 +68,7 @@ static uint8_t readFileHeader(FILE *file) {
} }
uint32_t missing = required & ~(uint32_t)SPLITBIT_FEATURES_SUPPORTED; uint32_t missing = required & ~(uint32_t)SPLITBIT_FEATURES_SUPPORTED;
if (missing) { if (missing) {
fprintf(stderr, "Error: This binary was built for a machine this emulator cannot provide.\n"); fprintf(stderr, "Error: This boot image was built for a machine this emulator cannot provide.\n");
fprintf(stderr, " It asks for feature bits 0x%08X, which are not implemented here.\n", missing); fprintf(stderr, " It asks for feature bits 0x%08X, which are not implemented here.\n", missing);
return 1; return 1;
} }
@@ -135,7 +135,7 @@ static uint8_t readVectorSegment(FILE *file, uint8_t *Program) {
return 1; return 1;
} }
if (slot < SOFTWARE_VECTOR_BASE) { if (slot < SOFTWARE_VECTOR_BASE) {
fprintf(stderr, "Error: This binary puts a vector at 0x%04X, which is below the vector table.\n", slot); fprintf(stderr, "Error: This boot image puts a vector at 0x%04X, which is below the vector table.\n", slot);
return 1; return 1;
} }
Program[slot] = (handler >> 8) & 0xFF; Program[slot] = (handler >> 8) & 0xFF;
+1 -1
View File
@@ -10,7 +10,7 @@
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 the binary format. // branch instructions and both file formats.
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];
+1 -1
View File
@@ -79,7 +79,7 @@ int main (int argc, char *argv[]) {
programFile = argv[optind]; programFile = argv[optind];
optind++; optind++;
} else { } else {
fprintf(stderr, "Error: No binary file specified.\n"); fprintf(stderr, "Error: No boot image specified.\n");
printHelp(argv[0]); printHelp(argv[0]);
return 1; return 1;
} }
+1 -1
View File
@@ -11,7 +11,7 @@
#include "../Assembler/assembly.h" #include "../Assembler/assembly.h"
void printHelp(const char *programName) { void printHelp(const char *programName) {
printf("Usage: %s [OPTIONS] <binaryfile>\n", programName); printf("Usage: %s [OPTIONS] <boot image>\n", programName);
printf("\n"); printf("\n");
printf("Options:\n"); printf("Options:\n");
printf(" -d, --debug Enable debug mode.\n"); printf(" -d, --debug Enable debug mode.\n");
+115 -17
View File
@@ -10,7 +10,7 @@ The first four say what kind of thing follows them. #Base says where a segment i
SplitBit programs must have a Program Segment. You define the start of a program with the #Program Keyword. SplitBit programs must have a Program Segment. You define the start of a program with the #Program Keyword.
SplitBit programs may have a Data Segment. You may define the start of the data with the #Data Keyword. SplitBit programs may have a Data Segment. You may define the start of the data with the #Data Keyword.
SplitBit programs may have a Vector Segment. You define it with the #Vectors Keyword. See The Vector Segment below. SplitBit programs may have a Vector Segment. You define it with the #Vectors Keyword. See The Vector Segment.
## Literal Values: ## Literal Values:
@@ -24,7 +24,7 @@ Any token beginning with a '0' is read as a numerical literal, so a malformed on
A string may be up to 255 characters. Each one is written down with a zero byte on the end, which is what lets a program find where it stops, and it means two strings written one after the other are not one longer string: there is a zero between them. A run of bytes longer than a string can hold has to be written as literals, or put there by the program itself while it runs. A string may be up to 255 characters. Each one is written down with a zero byte on the end, which is what lets a program find where it stops, and it means two strings written one after the other are not one longer string: there is a zero between them. A run of bytes longer than a string can hold has to be written as literals, or put there by the program itself while it runs.
**Strings belong in the Data Segment, and only there.** This is a Harvard machine: no instruction reads Program Memory, so a string put in the Program Segment could not be read by the program carrying it, and only the memory controller could reach it at all. The assembler refuses one rather than emitting bytes nothing can use. Single byte literals are a different matter and may go in either segment a table of bytes a program branches through is a reasonable thing to want in Program Memory. **Strings belong in the Data Segment, and only there.** This is a Harvard machine: no instruction reads Program Memory, so a string put in the Program Segment could not be read by the program carrying it, and only the memory controller could reach it at all. The assembler refuses one rather than emitting bytes nothing can use. Single byte literals are a different matter and may go in either segment - a table of bytes a program branches through is a reasonable thing to want in Program Memory.
The one exception to the single byte rule is #Align and #Reserve, whose numbers are never emitted as bytes and may go up to 0xFFFF. See Moving The Cursor Along. The one exception to the single byte rule is #Align and #Reserve, whose numbers are never emitted as bytes and may go up to 0xFFFF. See Moving The Cursor Along.
@@ -207,6 +207,104 @@ A program that genuinely wants a segment at the bottom of memory says so:
`#Base` is the one directive that takes zero. `#Align` and `#Reserve` are counts, and a count of nothing is a typo, so they still require at least one. `#Base` is the one directive that takes zero. `#Align` and `#Reserve` are counts, and a count of nothing is a typo, so they still require at least one.
## The Boot Image Format:
A boot image is what the machine starts from: the Program and Data segments in one file,
with nothing to say where they go, because they go at the bottom of each memory. It is what
the assembler writes when a program does not say where it lives, and what the emulator is
given on the command line.
Every value in it is stored most significant byte first, which is the same order the CPU reads addresses out of Program Memory.
A file begins with a nine byte header:
| Offset | Size | Field |
| -- | -- | -- |
| 0 | 4 | The characters `SPBT`, so that a file which is not a boot image is recognised as such straight away. |
| 4 | 1 | The format version. This document describes version 1. |
| 5 | 4 | Required feature flags. |
The feature flags are how a boot image states that it needs something the base machine does not provide. An emulator that cannot provide everything an image asks for refuses to run it, rather than running it and going quietly wrong. No feature bits are defined yet, so the field is currently zero in every image.
After the header come the segments. The Program Segment comes first and then the Data Segment, each beginning with a three character marker, `PRG` or `DAT`, followed by a two byte length. The system loads each memory with the bytes that follow, in sequence, starting from address 0x0000.
A third segment may follow them, marked `VEC`, holding the vectors a program named in its Vector Segment. It is four bytes an entry: two saying where in Program Memory the vector sits, and two saying where its handler is. A program that named no vectors has no such segment, and a file that simply ends after its Data Segment is one written before vectors existed. Either way the reader treats the end of the file as an empty table, which is why adding this cost no format version and left every image already written still loadable.
Here is the hello world program from the Programming Manual, assembled and dumped as hex:
```
53 50 42 54 01 00 00 00 00 50 52 47 00 11 42 00 12 00 0c d1 00 40 00 10 00 00 26 0a d1 00 ff 44
41 54 00 0e 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 00
```
Taken apart:
| Bytes | Meaning |
| -- | -- |
| `53 50 42 54` | `SPBT` |
| `01` | Format version 1 |
| `00 00 00 00` | No features required |
| `50 52 47` | `PRG` |
| `00 11` | The Program Segment is 17 bytes long |
| `42 00 12 00 0c d1 00 40 00 10 00 00 26 0a d1 00 ff` | The Program Segment |
| `44 41 54` | `DAT` |
| `00 0e` | The Data Segment is 14 bytes long |
| `48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 00` | The Data Segment |
The `42 00` at the start of the Program Segment is worth a look: `42` is LDA, and the `00` after it is the Data Pointer selector the assembler filled in, because the program did not name one.
## Loading A Program From A Disk:
A program that was not the one the machine booted from carries sixteen bytes in front of it saying where it belongs.
| Offset | Size | Holds |
| --- | --- | --- |
| 0 | 4 | SBEX |
| 4 | 1 | Version. One, or two if it brings vectors. |
| 5 | 1 | How many vectors follow the data. Zero in a version one file. |
| 6 | 2 | Where the code goes in Program Memory. |
| 8 | 2 | Where to start running. |
| 10 | 2 | How many bytes of code there are. |
| 12 | 2 | Where the data goes in Data Memory. |
| 14 | 2 | How many bytes of data there are. |
| 16 | | The code, then the data, then the vectors. |
Programs/Loader/loader.asm reads one off a disk, puts the two pieces where the header asks, and jumps to the entry with BRD. Every part of that already existed: the filesystem finds the file, the memory controller writes Program Memory, and BRD turns an address worked out at run time into somewhere to go. The header is the only new thing. Programs/CosmOS does the same as one of its commands, and then takes the machine back afterwards, which the standalone loader has no way to do.
The magic matters for the same reason it does everywhere else on this machine. Without it, loading a text file would put nonsense into Program Memory and then jump into it.
### Bringing Vectors:
A program that only wants to be run needs nothing here and says version one. A program that wants a handler installed needs something of whoever loads it, and says version two.
Each vector is four bytes: the address of the slot in the vector table, then the address to put in it, both most significant byte first. Naming the slot rather than the vector number means the loader does no arithmetic and does not have to know where either vector table begins, and one entry can be a software or a hardware vector without saying which it is.
**A version two file is refused by a loader that cannot install them.** That is the point of the version rather than an inconvenience of it. A program whose handlers were quietly dropped would load, run, and then go wrong somewhere with nothing to connect the failure back to loading - a game waiting for keys that no longer arrive. Failing once, at load, with a reason, is worth more than running.
**Whoever installs them takes them back.** A vector points into the program that supplied it, so one left in the table after that program has gone aims an interrupt at whatever occupies those addresses next. CosmOS keeps its own copy of what a program brought, puts them in when the program is run and not when it is loaded, and restores what was underneath them when the program gives the machine back. Restoring, rather than clearing: a program is allowed to install a handler over one the system was already using, and when it goes, what it covered up has to come back rather than become a hole.
`Boot` in a loadable program fills in the entry field, since that is what it means, and is not installed as vector 0 - where the machine starts is not a loaded program's business. Without one, a program begins at the first byte of its code.
### Where A Program Says It Lives:
**Nothing relocates anything.** A program is put exactly where its header asks, and that has to be the address it was assembled for, or every branch and every SETD inside it points somewhere wrong.
A program says where it lives with #Base, at the top of each segment. Every label inside is then resolved from there, so the addresses in the program and the addresses in its header say the same thing. Giving either segment a base is also what makes the assembler write the program out as a loadable one rather than as a boot image, carrying none of the empty space below it.
```
#Program
#Base 0x2000 ; This program's code lives from 0x2000.
hello:
...
#Data
#Base 0x1000 ; And its data from 0x1000.
```
That is not general placement: a base applies to a whole segment, and only the first thing in one can set it. It is exactly enough for a program that wants to live at one address, which is what a loadable program is.
Whoever does the loading keeps its own code and data below the addresses the loaded program claims. That is an arrangement between the two of them rather than anything the machine enforces. Programs/CosmOS is where that arrangement is written down as a memory map and kept to.
## The Vector Segment: ## The Vector Segment:
A vector says where to go when something happens: the machine starting up, a program asking for a service, a device wanting attention, or the CPU meeting a byte it cannot decode. The Vector Segment says which of your routines belongs to which vector, and the assembler works out the rest. A vector says where to go when something happens: the machine starting up, a program asking for a service, a device wanting attention, or the CPU meeting a byte it cannot decode. The Vector Segment says which of your routines belongs to which vector, and the assembler works out the rest.
@@ -233,7 +331,7 @@ A line with a name and nothing after it declares the name and its number without
osExit 0d17 osExit 0d17
``` ```
The numbers are there because this is the case where a number has to be agreed. Everything else in a Vector Segment is numbered by the assembler, which can only see one program at a time and that is exactly the situation where it cannot help. See Numbers You Write Down below. The numbers are there because this is the case where a number has to be agreed. Everything else in a Vector Segment is numbered by the assembler, which can only see one program at a time - and that is exactly the situation where it cannot help. See Numbers You Write Down.
Five names already mean something: Five names already mean something:
@@ -259,7 +357,7 @@ The assembler will refuse two handlers for the same vector, a name used with SWI
### Numbers You Write Down: ### Numbers You Write Down:
A vector number is worth arguing about in exactly one situation: when two programs that are **not assembled together** have to mean the same thing by a name. A program calling `osExit` was built long before, and separately from, whatever implements it. Nothing the assembler can see ties those two together, because it only ever sees one of them. A vector number is worth arguing about in exactly one situation: when two programs that are **not assembled together** have to mean the same thing by a name. A program calling `osExit` was built separately from whatever implements it. Nothing the assembler can see ties those two together, because it only ever sees one of them.
For that case a number may be written between the name and the handler. For that case a number may be written between the name and the handler.
@@ -282,7 +380,7 @@ The software vector space is divided so the two kinds cannot meet:
You may give a number only in the pinned range. Below it belongs to the machine, and above it is where the assembler is allocating, so a number claimed there could be handed to something else in the same breath. You may give a number only in the pinned range. Below it belongs to the machine, and above it is where the assembler is allocating, so a number claimed there could be handed to something else in the same breath.
**Why the split exists**, because it is not obvious and the reason is a real mistake that used to be possible. When both kinds came out of one range, the numbers a program got for its own traps depended on what it had included: adding a line that included a file naming three services pushed every trap after it along by three, and a program that had *not* included that file was given the first number in the range which was a service. Installing its own handler there would have quietly replaced one. Now numbers that must agree are written down, and numbers that need not agree come from somewhere nobody else is looking, so a program's own vectors are its own regardless of how it was built. **Why the split exists**, because it is not obvious and the reason is a real mistake that used to be possible. When both kinds came out of one range, the numbers a program got for its own traps depended on what it had included: adding a line that included a file naming three services pushed every trap after it along by three, and a program that had *not* included that file was given the first number in the range - which was a service. Installing its own handler there would have quietly replaced one. Now numbers that must agree are written down, and numbers that need not agree come from somewhere nobody else is looking, so a program's own vectors are its own regardless of how it was built.
The assembler will refuse a number outside the pinned range, two names given the same number, a number on `Boot`, `SoftReset` or `BadOpcode`, whose numbers are the machine's, and a number that contradicts one the same name was already given. The assembler will refuse a number outside the pinned range, two names given the same number, a number on `Boot`, `SoftReset` or `BadOpcode`, whose numbers are the machine's, and a number that contradicts one the same name was already given.
@@ -306,7 +404,7 @@ An absolute path is taken as it is written. If the file turns up nowhere, the as
Because a library is normally referred to by name alone, a program that uses one has to be told where the libraries live: Because a library is normally referred to by name alone, a program that uses one has to be told where the libraries live:
``` ```
Assembler -I Libraries primeSieve/8bitSieve.asm Assembler -I Libraries Examples/primeSieve/8bitSieve.asm
``` ```
Including the same file twice does nothing the second time, so two libraries may both depend on a third without the program that uses them having to know. The assembler compares files by where they really are rather than by how they were spelled, so the same library reached by two different routes is still only assembled once. Including the same file twice does nothing the second time, so two libraries may both depend on a third without the program that uses them having to know. The assembler compares files by where they really are rather than by how they were spelled, so the same library reached by two different routes is still only assembled once.
@@ -319,16 +417,16 @@ Assembler [options] <sourcefile>
| Option | Meaning | | Option | Meaning |
| -- | -- | | -- | -- |
| -o, --output \<file\> | Write the binary to this path. Without it, the binary is named after the source file, with a .bin extension, in the directory the assembler was run from. | | -o, --output \<file\> | Write the output to this path. Without it, the output is named after the source file, in the directory the assembler was run from, taking .bin if it is a boot image and .sbx if it is a loadable program. |
| -I, --include \<dir\> | Look in this directory for included files. May be given more than once, and the directories are searched in the order given. | | -I, --include \<dir\> | Look in this directory for included files. May be given more than once, and the directories are searched in the order given. |
| -M, --depend \<file\> | Write out which source files went into the binary, as a make rule. | | -M, --depend \<file\> | Write out which source files went into the output, as a make rule. |
| -h, --help | Print the options and stop. | | -h, --help | Print the options and stop. |
The assembler stops at the first error, says which file and line it was in, and exits without writing a binary. The assembler stops at the first error, says which file and line it was in, and exits without writing anything.
## Building With Make: ## Building With Make:
The -o and -M options are there so that the assembler fits into a build system. -o puts the binary wherever the build wants it, and -M writes down which libraries went into it, so that editing a library reassembles every program that includes it. The -o and -M options are there so that the assembler fits into a build system. -o puts the output wherever the build wants it, and -M writes down which libraries went into it, so that editing a library reassembles every program that includes it.
``` ```
$(BUILD)/%.bin: %.asm $(BUILD)/%.bin: %.asm
@@ -491,23 +589,23 @@ wrote hello.bin: program 17, data 14, labels 2
Loading and running are separate commands in CosmOS, so the source file is the argument to `run`. Loading and running are separate commands in CosmOS, so the source file is the argument to `run`.
**Its output must be byte for byte what the host assembler produces from the same source**, and `Tests/native.sh` checks exactly that: it assembles `Programs/hello.asm` both ways and compares the files, then runs the one the machine built. This is the discipline SplitDisk and `sbfs.asm` already work under two implementations of one written specification, each one checking the other. "It ran" is not good enough for an assembler, because a binary with a label one byte out runs right up until it jumps into the middle of an instruction. **Its output must be byte for byte what the host assembler produces from the same source**, and `Tests/native.sh` checks exactly that: it assembles `Programs/Examples/hello.asm` both ways and compares the files, then runs the one the machine built. This is the discipline SplitDisk and `sbfs.asm` already work under - two implementations of one written specification, each one checking the other. "It ran" is not good enough for an assembler, because a file with a label one byte out runs right up until it jumps into the middle of an instruction.
### How It Differs Inside: ### How It Differs Inside:
The host assembler reads every token of every file into one array and works on that. **That design cannot port and never could**: `cosmos.asm` alone is 56,047 bytes of source against 64K of Data Memory, and its token array would be several times that. So the native one streams its source through a 256 byte window, twice, and keeps only the label table between the passes. The host assembler reads every token of every file into one array and works on that. **That design cannot port and never could**: `cosmos.asm` alone is 56,047 bytes of source against 64K of Data Memory, and its token array would be several times that. So the native one streams its source through a 256 byte window, twice, and keeps only the label table between the passes.
Two passes are enough because **every length is known without resolving anything**. How many bytes a token comes to falls out of what the token is an instruction's from its shape, a value's is one, a string's is its characters and a zero and never from the value of anything named. So the first pass works out exactly where every label lands and the second never needs a fixup list. A forward reference stops being a special case and becomes the reason there are two passes at all. Two passes are enough because **every length is known without resolving anything**. How many bytes a token comes to falls out of what the token is - an instruction's from its shape, a value's is one, a string's is its characters and a zero - and never from the value of anything named. So the first pass works out exactly where every label lands and the second never needs a fixup list. A forward reference stops being a special case and becomes the reason there are two passes at all.
One thing is genuinely easier here than on a host. The host assembler searches a list of include directories, because a host has directories; **SBFS is flat**, so an include is a file name and there is nowhere else to look. One thing is genuinely easier here than on a host. The host assembler searches a list of include directories, because a host has directories; **SBFS is flat**, so an include is a file name and there is nowhere else to look.
### Building Applications: ### Building Applications:
`#Include` splices another file in where it stands, so the reader is a stack of readers: the current file's whole state goes aside, the new one opens, and the end of it pops the old one back. A file is included **once** including it twice is not an error, it just does nothing, which is what lets two libraries depend on a third. `#Include` splices another file in where it stands, so the reader is a stack of readers: the current file's whole state goes aside, the new one opens, and the end of it pops the old one back. A file is included **once** - including it twice is not an error, it just does nothing, which is what lets two libraries depend on a third.
`#Base` says where a segment is loaded, and a program that says so gets the SBEX loadable header instead of the SPBT boot one, with a `.sbx` name rather than a `.bin`. `#Reserve` and `#Align` lay down runs of zeroes; how many an `#Align` comes to depends on where the cursor has reached, which is why both passes keep a cursor rather than the second one keeping only a write pointer. `#Base` says where a segment is loaded, and a program that says so gets the SBEX loadable header instead of the SPBT boot one, with a `.sbx` name rather than a `.bin`. `#Reserve` and `#Align` lay down runs of zeroes; how many an `#Align` comes to depends on where the cursor has reached, which is why both passes keep a cursor rather than the second one keeping only a write pointer.
Names in `#Vectors` are read and numbered, pinned where the source pins them, so `SWI osPrintString` resolves. **What a name after `SWI` means is settled by what it follows**, not by anything about the name the Vector Segment may live in a file included further down and may not have been read yet. Names in `#Vectors` are read and numbered, pinned where the source pins them, so `SWI osPrintString` resolves. **What a name after `SWI` means is settled by what it follows**, not by anything about the name - the Vector Segment may live in a file included further down and may not have been read yet.
That is everything an application needs: That is everything an application needs:
@@ -535,7 +633,7 @@ A `#Vectors` line that names a **handler** says this program implements that vec
`Device` is named by the port it is plugged into, because that is what decides which vector it arrives through. `Device`, `Boot`, `SoftReset`, `BadOpcode`, `GuardViolation` and `BankFault` are matched **without regard to case**, the way mnemonics are: they are part of the language rather than names the programmer chose. `Device` is named by the port it is plugged into, because that is what decides which vector it arrives through. `Device`, `Boot`, `SoftReset`, `BadOpcode`, `GuardViolation` and `BankFault` are matched **without regard to case**, the way mnemonics are: they are part of the language rather than names the programmer chose.
**A declaration and an implementation are the same entry.** `services.asm` says a service is called `osPrintString` and has number 16; `cosmos.asm` says `osPrintString` is handled by `handlePrintString`. Both sides include the first file, so the name is met twice and the second time fills in the handler. That is what lets one shared file serve both a program that calls a service and the system that implements it and it is why the first pass declares and the second implements, a handler being an address and no address being known until every label has been placed. **A declaration and an implementation are the same entry.** `services.asm` says a service is called `osPrintString` and has number 16; `cosmos.asm` says `osPrintString` is handled by `handlePrintString`. Both sides include the first file, so the name is met twice and the second time fills in the handler. That is what lets one shared file serve both a program that calls a service and the system that implements it - and it is why the first pass declares and the second implements, a handler being an address and no address being known until every label has been placed.
### Self Hosting: ### Self Hosting:
@@ -551,13 +649,13 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
Both come out **byte for byte identical** to what the host assembler builds from the same source. `make run-cosmos` puts every source file on the disk, so this can be done rather than read about. Both come out **byte for byte identical** to what the host assembler builds from the same source. `make run-cosmos` puts every source file on the disk, so this can be done rather than read about.
**The check that matters most is the third one.** A binary that matches could still have been built by an assembler wrong in some way this particular source happens not to exercise. So `Tests/native.sh` boots the CosmOS that CosmOS built and has *that* assemble CosmOS again and the second generation is identical to the first, down to the cycle count. It is a fixed point, which means the machinery has been through itself. **The check that matters most is the third one.** A file that matches could still have been built by an assembler wrong in some way this particular source happens not to exercise. So `Tests/native.sh` boots the CosmOS that CosmOS built and has *that* assemble CosmOS again - and the second generation is identical to the first, down to the cycle count. It is a fixed point, which means the machinery has been through itself.
After that the host is a convenience rather than a necessity. After that the host is a convenience rather than a necessity.
CosmOS takes about 80 million cycles, which is eighty seconds of emulated time and under a second under `--fast`. Most of that is the label table: a straight walk of 475 names, several thousand times. Sorting it or bucketing it on the first character are both easy, and neither was worth writing before there was something to measure. CosmOS takes about 80 million cycles, which is eighty seconds of emulated time and under a second under `--fast`. Most of that is the label table: a straight walk of 475 names, several thousand times. Sorting it or bucketing it on the first character are both easy, and neither was worth writing before there was something to measure.
**The hardest thing it assembles is not the operating system, it is itself** 555 labels and 6,770 bytes of name against CosmOS's 475 and 5,881, and a larger output. That is what the buffer sizes are cut to. **The hardest thing it assembles is not the operating system, it is itself** - 555 labels and 6,770 bytes of name against CosmOS's 475 and 5,881, and a larger output. That is what the buffer sizes are cut to.
### What It Does Not Do Yet: ### What It Does Not Do Yet:
File diff suppressed because it is too large Load Diff
+80 -32
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Checks the manuals against the code. # Checks the manuals against the code, and the repository against its own rules.
# #
# Documentation goes stale quietly. An instruction added without a table row, or a count # Documentation goes stale quietly. An instruction added without a table row, or a count
# in a heading that nobody updated, is wrong in a way nothing notices until somebody # in a heading that nobody updated, is wrong in a way nothing notices until somebody
@@ -25,9 +25,40 @@ def read(path):
pm = read("SplitBit Programming Manual.md") pm = read("SplitBit Programming Manual.md")
am = read("SplitBit Assembler Manual.md") am = read("SplitBit Assembler Manual.md")
# The third manual. CosmOS is a system that runs ON SplitBit rather than part of it, so
# what it offers a program is documented with it and checked here alongside the other two.
cr = read("Programs/CosmOS/README.md")
asmc = read("Source/Assembler/assembly.c") asmc = read("Source/Assembler/assembly.c")
util = read("Source/Assembler/Assm-util.c") util = read("Source/Assembler/Assm-util.c")
# ---- Every tracked file is plain ASCII ----
#
# A standing rule of this repository, and nothing enforced it, so it drifted: 39 em dashes
# and an ellipsis had collected in the two manuals, all of them typed by something that
# helpfully substituted a nicer character.
#
# GIT LS-FILES IS READ NUL SEPARATED, and that is not fussiness. The obvious shell version
# of this check - looping over $(git ls-files) - splits on whitespace, so it looked for a
# file called "SplitBit" and reported the repository clean while both manuals had drifted.
# A check that cannot see the files with spaces in their names is worse than no check.
import subprocess
tracked = subprocess.run(["git", "ls-files", "-z"], capture_output=True).stdout
for name in tracked.split(b"\0"):
if not name:
continue
path = name.decode()
try:
text = open(path, encoding="utf-8").read()
except (UnicodeDecodeError, OSError):
continue
for number, line in enumerate(text.split("\n"), 1):
odd = sorted({c for c in line if ord(c) > 127})
if odd:
problems.append("%s line %d is not plain ASCII: %s"
% (path, number, ", ".join("%r (U+%04X)" % (c, ord(c)) for c in odd)))
break
# ---- Every instruction has a row, and every row is an instruction ---- # ---- Every instruction has a row, and every row is an instruction ----
# #
# A mnemonic begins with a letter, which is what keeps the offset and size columns of the # A mnemonic begins with a letter, which is what keeps the offset and size columns of the
@@ -139,10 +170,10 @@ else:
sbex = read("Source/Assembler/sbex.h") sbex = read("Source/Assembler/sbex.h")
offsets = {name: int(value) offsets = {name: int(value)
for name, value in re.findall(r'^#define (SBEX_[A-Z_]+_AT)\s+(\d+)$', sbex, re.M)} for name, value in re.findall(r'^#define (SBEX_[A-Z_]+_AT)\s+(\d+)$', sbex, re.M)}
if "## Loading A Program From A Disk:" not in pm: if "## Loading A Program From A Disk:" not in am:
problems.append("the Programming Manual has lost its loadable program section") problems.append("the Assembler Manual has lost its loadable program section")
else: else:
loading = pm.split("## Loading A Program From A Disk:")[1].split("\n## ")[0] loading = am.split("## Loading A Program From A Disk:")[1].split("\n## ")[0]
listed = [int(m) for m in re.findall(r'^\| (\d+) \| \d* \|', loading, re.M)] listed = [int(m) for m in re.findall(r'^\| (\d+) \| \d* \|', loading, re.M)]
for name, offset in sorted(offsets.items(), key=lambda pair: pair[1]): for name, offset in sorted(offsets.items(), key=lambda pair: pair[1]):
if offset not in listed: if offset not in listed:
@@ -197,10 +228,10 @@ implemented = {name for name in re.findall(r'^\s{2}(os[A-Za-z]+)\s+[a-zA-Z]', ve
if name in named} if name in named}
if not named: if not named:
problems.append("no services could be found in services.asm") problems.append("no services could be found in services.asm")
elif "## What A Program May Ask The System For:" not in pm: elif "## What A Program May Ask The System For:" not in cr:
problems.append("the Programming Manual has lost its services section") problems.append("the CosmOS README has lost its services section")
else: else:
section = pm.split("## What A Program May Ask The System For:")[1].split("\n## ")[0] section = cr.split("## What A Program May Ask The System For:")[1].split("\n## ")[0]
documented = set(re.findall(r'^\| (os[A-Za-z]+) \|', section, re.M)) documented = set(re.findall(r'^\| (os[A-Za-z]+) \|', section, re.M))
for name in sorted(implemented - documented): for name in sorted(implemented - documented):
problems.append("%s is a service the system implements and has no row in the" problems.append("%s is a service the system implements and has no row in the"
@@ -216,16 +247,16 @@ else:
# routine that no longer exists. The other direction is deliberately not checked: the ported # routine that no longer exists. The other direction is deliberately not checked: the ported
# programs are covered in the prose rather than given a row each. # programs are covered in the prose rather than given a row each.
import os import os
if "## Programs That Come With The System:" not in pm: if "## Included Applications:" not in cr:
problems.append("the Programming Manual has lost its list of programs") problems.append("the CosmOS README has lost its list of applications")
else: else:
listed = pm.split("## Programs That Come With The System:")[1].split("\n### ")[0] listed = cr.split("## Included Applications:")[1].split("\n### ")[0]
# After the separator, so the table's own heading row is not mistaken for a program. # After the separator, so the table's own heading row is not mistaken for a program.
listed = listed.split("| --- |")[-1] listed = listed.split("| --- |")[-1]
for name in re.findall(r'^\| ([A-Z][A-Za-z0-9-]*) \|', listed, re.M): for name in re.findall(r'^\| ([A-Z][A-Za-z0-9-]*) \|', listed, re.M):
if not os.path.exists("Programs/CosmOS/Apps/%s.asm" % name): if not os.path.exists("Programs/CosmOS/Apps/%s.asm" % name):
problems.append("the manual describes a program called %s, and there is no" problems.append("the CosmOS README describes an application called %s, and"
" Programs/CosmOS/Apps/%s.asm" % (name, name)) " there is no Programs/CosmOS/Apps/%s.asm" % (name, name))
# ---- The monitor's instruction table is the assembler's ---- # ---- The monitor's instruction table is the assembler's ----
# #
@@ -296,13 +327,13 @@ for directive in sorted(set(re.findall(r'"(#[A-Za-z]+)"', util))):
# The first column of the table in each of these sections names something the library has # The first column of the table in each of these sections names something the library has
# to define. A routine renamed in the source and not in the manual is caught here, which # to define. A routine renamed in the source and not in the manual is caught here, which
# is what keeps the tables a description rather than a memory. # is what keeps the tables a description rather than a memory.
for heading, library in [("## Reading And Writing The Filesystem:", "Programs/CosmOS/Source/sbfs.asm"), for heading, library in [("## The Filesystem Library:", "Programs/CosmOS/Source/sbfs.asm"),
("## The Console Library:", "Programs/CosmOS/Source/console.asm")]: ("## The Console Library:", "Programs/CosmOS/Source/console.asm")]:
if heading not in pm: if heading not in cr:
problems.append("the Programming Manual has lost its \"%s\" section" problems.append("the CosmOS README has lost its \"%s\" section"
% heading.strip("# :")) % heading.strip("# :"))
continue continue
section = pm.split(heading)[1].split("\n## ")[0] section = cr.split(heading)[1].split("\n## ")[0]
defined = set(re.findall(r'^([a-zA-Z][A-Za-z0-9]*):', read(library), re.M)) defined = set(re.findall(r'^([a-zA-Z][A-Za-z0-9]*):', read(library), re.M))
for name in re.findall(r'^\| ([a-z][A-Za-z0-9]*) \|', section, re.M): for name in re.findall(r'^\| ([a-z][A-Za-z0-9]*) \|', section, re.M):
if name not in defined: if name not in defined:
@@ -316,22 +347,39 @@ import os
import subprocess import subprocess
import tempfile import tempfile
source = pm.split("### Example Program: Hello World")[1].split("```")[1] # BOTH ANCHORS ARE CHECKED BEFORE THEY ARE USED. Every other heading this script splits on
claimed = pm.split("assembled and dumped as hex:")[1].split("```")[1].split() # says what it could not find; these two were the exception, and a rename here produced an
with tempfile.TemporaryDirectory() as work: # IndexError and a traceback instead of a sentence. That is a worse answer than a stale
asm = os.path.join(work, "hello.asm") # manual, because whoever reads it learns nothing about which heading moved.
binary = os.path.join(work, "hello.bin") # THE TWO HALVES ARE IN DIFFERENT MANUALS NOW, and that makes this a better check than it
open(asm, "w").write(source) # was. The program belongs with the machine, where it arrives just after the instruction
built = subprocess.run(["./Assembler", asm, "-o", binary], # list; the hex dump belongs with the boot image format it is an example of, which is the
capture_output=True) # assembler's business. So this settles three things against each other at once: what the
if built.returncode != 0: # Programming Manual prints, what the Assembler Manual prints, and what the assembler does.
problems.append("the hello world program in the manual no longer assembles") exampleAnchor = "### Example Program: Hello World"
else: dumpAnchor = "assembled and dumped as hex:"
actual = ["%02x" % b for b in open(binary, "rb").read()] if exampleAnchor not in pm:
if [c.lower() for c in claimed] != actual: problems.append("the Programming Manual has lost its \"Example Program: Hello World\""
problems.append("the hex dump in the manual is not what that program assembles to" " heading, so the worked example cannot be found")
" now: it prints %d bytes and the assembler makes %d" elif dumpAnchor not in am:
% (len(claimed), len(actual))) problems.append("the Assembler Manual no longer says \"%s\" before the hex dump, so"
" there is nothing to compare the worked example against" % dumpAnchor)
else:
source = pm.split(exampleAnchor)[1].split("```")[1]
claimed = am.split(dumpAnchor)[1].split("```")[1].split()
with tempfile.TemporaryDirectory() as work:
asm = os.path.join(work, "hello.asm")
binary = os.path.join(work, "hello.bin")
open(asm, "w").write(source)
built = subprocess.run(["./Assembler", asm, "-o", binary], capture_output=True)
if built.returncode != 0:
problems.append("the hello world program in the manual no longer assembles")
else:
actual = ["%02x" % b for b in open(binary, "rb").read()]
if [c.lower() for c in claimed] != actual:
problems.append("the hex dump in the manual is not what that program"
" assembles to now: it prints %d bytes and the assembler"
" makes %d" % (len(claimed), len(actual)))
# ---- The Assembler Manual's worked programs still assemble ---- # ---- The Assembler Manual's worked programs still assemble ----
for heading in ["## An Example SplitBit Assembly Program:", for heading in ["## An Example SplitBit Assembly Program:",
+2 -2
View File
@@ -46,7 +46,7 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
# bytes, so that what gets loaded is always built from the source beside it. # bytes, so that what gets loaded is always built from the source beside it.
"$TOOL" format "$DISKS/load.img" 64 2 >/dev/null "$TOOL" format "$DISKS/load.img" 64 2 >/dev/null
# The assembler writes a loadable program itself, because the source says where it goes. # The assembler writes a loadable program itself, because the source says where it goes.
"$ROOT/Assembler" "$ROOT/Programs/loadable/hello.asm" -o "$WORK/hello.sbx" >/dev/null "$ROOT/Assembler" "$ROOT/Programs/Loader/loadable.asm" -o "$WORK/hello.sbx" >/dev/null
"$TOOL" put "$DISKS/load.img" "$WORK/hello.sbx" >/dev/null "$TOOL" put "$DISKS/load.img" "$WORK/hello.sbx" >/dev/null
# A disk for CosmOS. greet.sbx asks the system for everything it does rather than talking # A disk for CosmOS. greet.sbx asks the system for everything it does rather than talking
@@ -170,7 +170,7 @@ awk 'BEGIN { for (i = 0; i < 50; i++) printf "small %05d\n", i }' > small.txt
# oldest program in the repository: the first thing this machine ever ran is the first # oldest program in the repository: the first thing this machine ever ran is the first
# thing it assembles for itself. # thing it assembles for itself.
"$TOOL" format "$DISKS/asm.img" 2048 4 >/dev/null "$TOOL" format "$DISKS/asm.img" 2048 4 >/dev/null
cp "$ROOT/Programs/hello.asm" hello.asm cp "$ROOT/Programs/Examples/hello.asm" hello.asm
"$TOOL" put "$DISKS/asm.img" hello.asm >/dev/null "$TOOL" put "$DISKS/asm.img" hello.asm >/dev/null
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" -I "$ROOT/Programs/CosmOS/Assembler" \ "$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" -I "$ROOT/Programs/CosmOS/Assembler" \
"$ROOT/Programs/CosmOS/Assembler/readTest.asm" -o "$WORK/readTest.sbx" >/dev/null "$ROOT/Programs/CosmOS/Assembler/readTest.asm" -o "$WORK/readTest.sbx" >/dev/null
+13 -13
View File
@@ -30,16 +30,16 @@
# on what a program prints. # on what a program prints.
# ---- Programs that halt on their own ---- # ---- Programs that halt on their own ----
hello | hello.asm | run | - | - hello | Examples/hello.asm | run | - | -
printHello | printHello.asm | run | - | - printHello | Examples/printHello.asm | run | - | -
8bitFibonacci | Fibonacci/8bitFibonacci.asm | run | - | - 8bitFibonacci | Examples/Fibonacci/8bitFibonacci.asm | run | - | -
16bitFibonacci | Fibonacci/16bitFibonacci.asm | run | - | - 16bitFibonacci | Examples/Fibonacci/16bitFibonacci.asm | run | - | -
32bitFibonacci | Fibonacci/32bitFibonacci.asm | run | - | - 32bitFibonacci | Examples/Fibonacci/32bitFibonacci.asm | run | - | -
8bitSieve | primeSieve/8bitSieve.asm | run | - | - 8bitSieve | Examples/primeSieve/8bitSieve.asm | run | - | -
16bitSegmentedSieve | primeSieve/16bitSegmentedSieve.asm | run | - | - 16bitSegmentedSieve | Examples/primeSieve/16bitSegmentedSieve.asm | run | - | -
# The four pointer rewrite. It emits exactly the same primes as the line above, which # The four pointer rewrite. It emits exactly the same primes as the line above, which
# is the whole point of keeping both: the pair is a direct before and after. # is the whole point of keeping both: the pair is a direct before and after.
16bitSegmentedSieveModern | primeSieve/16bitSegmentedSieveModern.asm | run | - | - 16bitSegmentedSieveModern | Examples/primeSieve/16bitSegmentedSieveModern.asm | run | - | -
mathTest | testPrograms/mathTest.asm | run | - | - mathTest | testPrograms/mathTest.asm | run | - | -
printTest | testPrograms/printTest.asm | run | - | - printTest | testPrograms/printTest.asm | run | - | -
int16print | Libraries/int16print.asm | run | - | - int16print | Libraries/int16print.asm | run | - | -
@@ -86,7 +86,7 @@ sbfsEditTest | testPrograms/sbfsEditTest.asm | run | -
# Loading a program off a disk and running it. Everything below this line existed before # Loading a program off a disk and running it. Everything below this line existed before
# the loader did; the only new part is the sixteen bytes on the front of a loadable # the loader did; the only new part is the sixteen bytes on the front of a loadable
# program saying where it goes. # program saying where it goes.
loader | loader.asm | run | - | - | disks/load.img loader | Loader/loader.asm | run | - | - | disks/load.img
# Storage. The image is made fresh for each run, so block 3 starts as zeroes. # Storage. The image is made fresh for each run, so block 3 starts as zeroes.
diskTest | testPrograms/diskTest.asm | run | - | - | disk.img diskTest | testPrograms/diskTest.asm | run | - | - | disk.img
@@ -348,20 +348,20 @@ asm-tokenTest | CosmOS/Assembler/tokenTest.asm | assemble | -
asm-scratch | CosmOS/Assembler/scratch.asm | assemble | - | - asm-scratch | CosmOS/Assembler/scratch.asm | assemble | - | -
# ---- Programs driven by console input ---- # ---- Programs driven by console input ----
inputTest | inputTest.asm | run | inputTest.in | - inputTest | Examples/inputTest.asm | run | inputTest.in | -
inputTestOld | testPrograms/inputTest.asm | run | inputTest.in | - inputTestOld | testPrograms/inputTest.asm | run | inputTest.in | -
replCalculator | replCalculator.asm | run | replCalculator.in | - replCalculator | Examples/replCalculator.asm | run | replCalculator.in | -
# ---- Programs that run forever by design, bounded by a cycle count ---- # ---- Programs that run forever by design, bounded by a cycle count ----
# 3,000,000 cycles is about fourteen generations of the glider, which puts # 3,000,000 cycles is about fourteen generations of the glider, which puts
# evolveBoard and its pointer juggling through its paces many times over. # evolveBoard and its pointer juggling through its paces many times over.
16x16Life | gameOfLife/16x16Life.asm | run | - | 3000000 16x16Life | Examples/gameOfLife/16x16Life.asm | run | - | 3000000
# The four pointer rewrite, on the same budget so the two can be compared directly. # The four pointer rewrite, on the same budget so the two can be compared directly.
# Note that this cannot show a speed difference: frameDelay is 255 by 255 and swamps # Note that this cannot show a speed difference: frameDelay is 255 by 255 and swamps
# the simulation, so both versions render the same fourteen generations and produce # the simulation, so both versions render the same fourteen generations and produce
# identical bytes. What it checks is that the rewrite still evolves the board the same # identical bytes. What it checks is that the rewrite still evolves the board the same
# way, which is what a regression test is for. # way, which is what a regression test is for.
16x16LifeModern | gameOfLife/16x16LifeModern.asm | run | - | 3000000 16x16LifeModern | Examples/gameOfLife/16x16LifeModern.asm | run | - | 3000000
# ---- Libraries: no entry point, so only check that they assemble ---- # ---- Libraries: no entry point, so only check that they assemble ----
# The CosmOS libraries assemble on their own, unlike print.asm below, which cannot: it # The CosmOS libraries assemble on their own, unlike print.asm below, which cannot: it
+2 -2
View File
@@ -57,7 +57,7 @@ mkdir -p "$WORK"
-o "$WORK/Asm.sbx" "$ROOT/Programs/CosmOS/Assembler/Asm.asm" >/dev/null || exit 1 -o "$WORK/Asm.sbx" "$ROOT/Programs/CosmOS/Assembler/Asm.asm" >/dev/null || exit 1
"$TOOL" format "$WORK/native.img" 2048 4 >/dev/null "$TOOL" format "$WORK/native.img" 2048 4 >/dev/null
"$TOOL" put "$WORK/native.img" "$ROOT/Programs/hello.asm" hello.asm >/dev/null "$TOOL" put "$WORK/native.img" "$ROOT/Programs/Examples/hello.asm" hello.asm >/dev/null
"$TOOL" put "$WORK/native.img" "$WORK/Asm.sbx" Asm.sbx >/dev/null "$TOOL" put "$WORK/native.img" "$WORK/Asm.sbx" Asm.sbx >/dev/null
# The applications, and the file of service names they all include. These are the reason # The applications, and the file of service names they all include. These are the reason
@@ -86,7 +86,7 @@ done
check "it wrote a file" grep -q "wrote hello.bin" "$WORK/session.txt" check "it wrote a file" grep -q "wrote hello.bin" "$WORK/session.txt"
# ---- And the file is the one the host assembler makes ---- # ---- And the file is the one the host assembler makes ----
"$ASM" -o "$WORK/reference.bin" "$ROOT/Programs/hello.asm" >/dev/null "$ASM" -o "$WORK/reference.bin" "$ROOT/Programs/Examples/hello.asm" >/dev/null
"$TOOL" get "$WORK/native.img" hello.bin "$WORK/native.bin" >/dev/null 2>&1 "$TOOL" get "$WORK/native.img" hello.bin "$WORK/native.bin" >/dev/null 2>&1
if [ -f "$WORK/native.bin" ]; then if [ -f "$WORK/native.bin" ]; then
REPORT="$(wc -c < "$WORK/native.bin" | tr -d ' ') bytes" REPORT="$(wc -c < "$WORK/native.bin" | tr -d ' ') bytes"