Programs/makefile is gone and everything it did is here. The split was not paying for itself: nothing at this level ever ran that file, so it rotted, and all three bugs found in it this week share that one cause. It named two source files that had been renamed months earlier and failed outright. The disk did not depend on the tree it mirrors, so a new file silently was not on it. And the disk was in no default target, so 'make clean' threw it away and 'make' did not bring it back - which is what prompted this. The platform and the system stay separate, as TARGETS rather than as files: 'make SplitBit Assembler' builds the machine and its tools, 'make cosmos' and 'make disk' build the system, and somebody who wants to write their own system can ignore the second group entirely. That boundary is now one make enforces, which the directory boundary never did - Tests/makedisks.sh has always reached across it to build its own fixtures. 'make' now builds a bootable disk as well as the tools, because a machine with nothing in the drive does not do anything and the first thing anybody wants after building this is to watch CosmOS come up. It costs half a second: the whole system, twenty six apps and the native assembler assemble in less time than the emulator links. Two things the merge needed that the split did not. Assembling now takes an order-only dependency on the assembler, which came free when you had already built the tools before changing directory. And the mirror is told which directory to walk: it was ".", meaning Programs/, and left alone it would have meant the whole repository - the C sources, the tests and the manuals mirrored onto a disk for an 8-bit machine. The disk comes out at 188 files where it was 189. The one that went is Programs/makefile, which was itself being mirrored onto it as /Source/makefile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
37 KiB
The SplitBit Assembler Manual:
SplitBit assembly syntax is similar to many other assembler syntaxes. Whitespace at the start or end of a line is disregarded by the assembler and may be used to make programs more readable to the programmer. The Instruction Mnemonics are listed in the SplitBit Programming Manual, and the assembler is not case sensitive in regard to the mnemonics.
A semicolon, ';', denotes the start of a comment, anything beyond it on a line is disregarded by the assembler.
Special Keywords are denoted with hash marks, '#'. The Keywords are #Include, #Program, #Data, #Vectors, #Base, #Align, and #Reserve.
The first four say what kind of thing follows them. #Base says where a segment is loaded, and is described under Programs Meant To Be Loaded. #Align and #Reserve are instructions to the assembler in the middle of a segment, and are described under Moving The Cursor Along.
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 Vector Segment. You define it with the #Vectors Keyword. See The Vector Segment.
Literal Values:
Literal values may be defined in a few ways. Numerical values must be within the range of a single 8 bit integer. The assembler will accept:
- Hexadecimal values prefaced with 0x, eg. 0x00, 0x7F.
- Decimal values prefaced with 0d, eg. 0d0, 0d120, 0d255.
- Strings enclosed in double quotes, eg. "a", "Hello, World!", "It is dark, you are likely to be eaten by a grue."
Any token beginning with a '0' is read as a numerical literal, so a malformed one is an error rather than something the assembler tries to interpret as a label. This also means a label cannot begin with a '0'.
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.
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.
Labels:
Labels may be a string of up to 32 alphanumeric characters that must end with a colon, ':'.
programStart:
loopStart:
errorHandler01:
A name may only be defined once across a program and everything it includes. Defining it twice is an error, because otherwise a reference resolves to whichever definition came first, and a typo or a name that two libraries both happen to use is very hard to track down.
A label may be referenced by name, without the colon, to place its two byte address wherever the reference appears.
In the Program Segment that is how the branch instructions and SETD are given somewhere to go. In the Data Segment it writes the address down as data, which is how a table of addresses is built for LDD to walk.
#Data
One:
"one"
Two:
"two"
Table: ; Two entries, each the two byte address of a string above.
One
Two
Naming a Data Pointer:
The instructions that work through a Data Pointer name which one by hanging a selector off the mnemonic, after a full stop.
LDA.2 ; Load A through Data Pointer 2.
STA.1 ; Store A through Data Pointer 1.
INCD.2 ; Step Data Pointer 2 along.
SETD.3 Grid ; Aim Data Pointer 3 at Grid.
Leave the selector off and the instruction uses Data Pointer 0, so a program that only needs one pointer never has to write one.
LDA ; Exactly the same as LDA.0
LDD and STD move a pointer through a pointer, so they take two selectors. The first names the pointer being moved and the second names the pointer that addresses it. Either may be left off, and again means Data Pointer 0.
LDD.1.0 ; Data Pointer 1 becomes the address stored at Data Pointer 0.
STD.1.0 ; Store Data Pointer 1 into the memory addressed by Data Pointer 0.
LDD.2 ; Same as LDD.2.0
LDD ; Same as LDD.0.0, which makes DP0 follow the address it holds.
Writing a selector on an instruction that does not work through a Data Pointer is an error, as is naming a pointer the machine does not have, or giving an instruction more selectors than it takes.
Instruction Operands:
Instructions that read operand bytes out of Program Memory must be followed by those operands. The branch instructions and CALL take a label; SETD takes a label or a pair of literal bytes; INIA, INIB, DPUP, DPDN, and the input and output instructions each take a single literal byte; SWI takes the name of a vector, or a literal number.
Leaving an operand off is an error rather than something the assembler works around, because the instruction would otherwise take whatever followed it as the operand and every address after that would shift.
Data Pointer selectors do not count as operands here, because they are written on the mnemonic rather than after it.
Moving The Cursor Along:
Both segments are written from the beginning, and every label stands for wherever the cursor had reached when the assembler met it. Two directives move that cursor without you having to write zeroes by hand.
#Align puts down as many zero bytes as it takes to reach the next multiple of the number that follows it.
#Data
#Align 0x100
Segment: ; Guaranteed to begin at a page boundary.
This matters for code that does address arithmetic on a pointer's low byte and treats the carry out as reaching the end of something. Both prime sieves work that way, and both now ask for the boundary themselves. Before this existed they relied on print.asm padding its data out to a whole page, which worked but put the requirement in a different file from the code that needed it, and quietly charged every other program 253 bytes for it.
#Reserve puts down the number of zero bytes that follows it, so that a label can stand for a whole region rather than just its first byte.
#Data
Buffer:
#Reserve 0d256 ; Anything after this begins 256 bytes further on.
Next:
Without it a label like Buffer is one byte as far as the assembler knows, so a later label lands inside the region and the two quietly overlap.
Both take a number written the way literals are, prefaced with 0x or 0d, but the number may go up to 0xFFFF rather than being held to a single byte. Neither number is ever emitted, so a byte's range would be the wrong limit: a page alignment needs 256, and a reservation is often much larger.
Both work in the Program Segment as well as the Data Segment, and both are an error anywhere else, because outside a segment there is no cursor to move.
Programs Meant To Be Loaded:
A program assembled without saying anything about where it goes is a boot image. Both its segments begin at zero, which is where the machine puts them, and it is written out in the format the emulator loads.
A program that will be loaded by something else has to say where it belongs, because nothing relocates it. #Base says so, and it has to be the first thing in its segment:
#Program
#Base 0x2000 ; This program's code lives from 0x2000.
start:
...
#Data
#Base 0x1000 ; And its data from 0x1000.
Message:
"..."
Every label inside is then already the address it will have once the program is loaded, so a branch or a SETD written in it points at the right place. Giving either segment a base makes the whole program a loadable one, and the assembler writes it out with a header saying where its two pieces go, followed by the pieces themselves. The space below each base is not in the file: the header says where the bytes belong and the loader puts them there.
A program starts at its code base unless it says otherwise, and Boot in its Vector Segment is how it says otherwise:
#Program
#Base 0x2000
helpers:
...
start:
...
#Vectors
Boot start ; Which is where this program begins.
That fills in the entry point in the header. It is not installed as vector 0 of the machine, which is where everything begins at power on and no business of a program being loaded into a system that is already running.
Vectors In A Loadable Program:
Everything else in a Vector Segment is carried in the file and installed by whatever loads the program. That is what lets a loaded program be interrupted: a handler is an address in the vector table, and until the format could carry one, a program that was not the one the machine booted from had no way to ask for it.
#Vectors
Boot start
Device 0x00 keyHandler ; The console, which now interrupts this program.
A program carrying vectors is written out as version two of the format, and the assembler says so:
Vectors: 1. Version 2, so a loader that cannot install them will say so.
A program carrying none stays version one and loads anywhere. The difference matters because a loader that does not understand version two refuses the file rather than running a program with its handlers missing, which would work until the moment it was supposed to be interrupted and then fail somewhere with nothing pointing back at the cause.
Whoever loads the program is expected to take the vectors out again when it finishes. See the Programming Manual.
The address a program is assembled for has to be the address it is loaded at. Nothing checks that, and nothing can fix it: a program put anywhere else has every branch and every SETD inside it pointing somewhere wrong.
Both Segments Or Neither:
Base one segment and the assembler expects a base on the other, if the other holds anything. Forgetting the second one is refused:
Error: The Program Segment is based at 0x2000, but the Data Segment
has 3 bytes at 0x0000 and was never given a #Base.
Half a program loaded at zero lands on whatever is already there.
This is worth refusing rather than allowing, because the result runs. The unbased half keeps the addresses it was given, which count up from zero, and the loader puts it exactly there, on top of whatever the system keeps at the bottom of memory. Nothing fails at load and nothing fails at the jump. It fails later, somewhere else, as corruption of something that never went near the program that caused it.
It is easy to do by accident. A segment can come from an included library rather than from the program itself, and a #Data that arrives with #Include print.asm is just as unbased as one you wrote, while being much harder to notice missing.
A program that genuinely wants a segment at the bottom of memory says so:
#Data
#Base 0x0000 ; Meant, not forgotten.
#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 62 00 0c d1 00 40 00 60 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 62 00 0c d1 00 40 00 60 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:
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 program does not need one. Without a Vector Segment a program starts at the beginning and behaves exactly as it always has.
Every line names a vector and then the label of the routine that handles it.
#Vectors
Boot realStart
BadOpcode reportFault
openFile openFileHandler
Device 0x10 diskReady
A line with a name and nothing after it declares the name and its number without installing anything. That is what lets one file be included by both the program that provides a service and the program that calls it: the shared file names them, the provider follows it with handlers, and a program that only calls them says them by name without pretending to implement them.
; services.asm, included by both sides
#Vectors
osPrint 0d16
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.
Five names already mean something:
| Name | Vector |
|---|---|
| Boot | Where the machine begins at power on. Without this a program starts at the beginning of its Program Segment. |
| SoftReset | A warm restart. SWI SoftReset is how a program asks for one. |
| BadOpcode | The CPU met a byte that is not an instruction. |
| GuardViolation | A device refused a write, because it landed inside a raised fence. |
| BankFault | A bank was named that has nothing in it, or an access ran past its end. |
Anything else you name is a software interrupt of your own. Usually you do not choose its number: the assembler allocates them in the order they appear, from vector 64 upwards. That is the same bargain as labels everywhere else in SplitBit assembly, where you name a thing and let the assembler work out where it went.
You then use the name as the operand of SWI:
SWI openFile
A device is different, because its number is not a choice. A device interrupts on the port it is plugged into, so the Device line says which port rather than giving it a name of its own. The port is a literal value, and the routine after it handles that device.
The assembler will refuse two handlers for the same vector, a name used with SWI that no Vector Segment gives a handler to, and a handler that is not a label.
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 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.
#Vectors
osExit 0d18 handleExit ; pinned, and implemented here
osPrint 0d16 ; pinned, implemented by somebody else
openFile openHandler ; the assembler picks the number
The software vector space is divided so the two kinds cannot meet:
| Vectors | Whose |
|---|---|
| 0 to 2 | Boot, SoftReset and BadOpcode. The machine's, and fixed. |
| 3 to 15 | Faults. Named as each is defined; the rest are held back. |
| 16 to 63 | Pinned. Never handed out. A number here is written down or it is not used. |
| 64 to 255 | Automatic. Handed out in order. These belong to one program and nothing outside it can name them. |
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.
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.
Vectors 3 through 15 are held back for faults, most of which have not been defined yet. They have no names, so there is currently no way to write a handler for one, and none is needed: each will be given a name of its own as the fault it stands for is defined, the way GuardViolation and BankFault were. Since a number may only be written in the pinned range, there is no way to land on one of them by accident either.
Including Other Files:
The #Include Keyword tells the assembler to load another file to be assembled along with the current file. It is more or less equivalent to copying the contents of the included file into the current file being processed. You simply put the name of the file to include after the keyword.
#Include print.asm
The assembler looks for that file in two places, in this order:
- Beside the file that asked for it. A library including its own siblings needs no help.
- Along the include directories given with -I on the command line, in the order they were given.
An absolute path is taken as it is written. If the file turns up nowhere, the assembler says so and lists every place it looked.
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 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.
Running the Assembler:
Assembler [options] <sourcefile>
| Option | Meaning |
|---|---|
| -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. |
| -M, --depend <file> | Write out which source files went into the output, as a make rule. |
| -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 anything.
Building With Make:
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
@mkdir -p $(@D)
$(ASM) -I Libraries -M $(@:.bin=.d) -o $@ $<
-include $(BINARIES:.bin=.d)
The makefile in this repository builds every program that way, if you would like a longer example to copy.
An Example SplitBit Assembly Program:
; This is a slightly more advanced hello world program that demonstrates some SplitBit programming conventions.
#Program
start: ; By unenforced convention, Program Labels start with a lowercase letter.
SETD HelloString ; Set the Data Pointer to the address of the string.
CALL printString ; Call the string printing subroutine.
HALT ; End the program.
; This is a reusable subroutine that could be included in other programs.
printString: ; Expects Data Pointer to be set to the beginning of the string to be printed.
LDA ; Move the first character of the string into A.
BRA printDone ; If A is NULL, the string is finished, so return.
OUTA 0x00 ; Output the character.
INCD ; Increment Data Pointer to the next character.
BRI printString ; Branch to the beginning of the loop.
printDone:
RET ; Return to the caller.
#Data
HelloString: ; By unenforced convention, Data Labels start with a capital letter.
"Hello, World!"
An Example Using More Than One Data Pointer:
Copying between two places in Data Memory needs two pointers: one to read through and one to write through. With a single pointer this loop has to save and restore it on every pass.
; Copy a string from one place in Data Memory to another.
#Program
start:
SETD.0 Source ; DP0 walks the source.
SETD.1 Dest ; DP1 walks the destination.
copy:
LDA.0 ; Read a byte through DP0.
BRA copyDone ; A zero byte is the end of the string.
STA.1 ; Write it through DP1.
INCD.0
INCD.1
BRI copy
copyDone:
HALT
#Data
Source:
"Copied through two pointers."
Dest:
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Remember that DP0, DP1 and DP2 survive a CALL, so a loop like this one can call a subroutine in the middle without losing either pointer. DP3 does not survive, which is what makes it the pointer a subroutine uses to hand an address back.
An Example Using Interrupts:
This program installs three handlers and never writes a vector number. The Boot Vector sends the machine somewhere other than the first byte of the program, a trap the program names for itself is reached with SWI, and the test device on port 0x10 is caught when it asks for attention.
; Interrupt handling from all three directions.
#Program
start:
CIF ; Hold devices off while we set up.
SETD.0 Greeting
CALL printString
SWI announce ; A trap of our own, reached by name.
INIA 0d1
OUTA 0x10 ; Ask the test device for attention. Its line goes up.
SIF ; Let it through. It is answered before the next instruction.
HALT
; A trap. It is entered with a full frame, so it may use any register it likes
; without agreeing anything with the code it interrupted.
announce:
SETD.0 Trapped
CALL printString
RETI
; The device handler. Reached because the device sits on port 0x10.
deviceReady:
SETD.0 Device
CALL printString
RETI
; The fault handler. It reports and stops, rather than trying to carry on.
reportFault:
SETD.0 Broken
CALL printString
HALT
printString: ; Expects DP0 to be set to the beginning of the string.
LDA.0
BRA printDone
OUTA 0x00
INCD.0
BRI printString
printDone:
INIA 0x0A
OUTA 0x00
RET
#Data
Greeting:
"ready"
Trapped:
"trap"
Device:
"device"
Broken:
"bad opcode"
#Vectors
Boot start ; Begin here rather than at the first byte.
BadOpcode reportFault
announce announce ; A name of our own. The assembler numbers it.
Device 0x10 deviceReady ; Named by the port, because that is what decides it.
The output is ready, trap, then device.
Note that a vector name and a routine name are kept apart, so naming both announce is allowed. If that reads as confusing, name them differently: nothing requires them to match.
The Assembler That Runs On SplitBit:
There are two assemblers now. This manual has been describing the one that runs on a host and writes a file; Programs/CosmOS/Assembler/ holds one written in SplitBit assembly that runs on the machine itself, under CosmOS, and reads its source off a SplitBit disk.
> load Asm.sbx
> run hello.asm
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.
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:
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.
Both assemblers search for an include, and they search differently because they run on different machines. The host one walks the list given with -I, in order, after looking beside the file that asked. The native one looks in the working directory and then in /Lib - two places, both fixed, because a list somebody could set would need somewhere to live between one boot and the next and CosmOS has no such place yet.
That search is what lets a disk be organised at all. Without it every source that calls a service would have to sit in the same directory as services.asm, and a disk would be one long list of names whatever the filesystem could do. It was one long list until SBFS grew directories: an include used to be a bare name with nowhere else to look, because there was nowhere else.
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.
#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.
That is everything an application needs:
> load Asm.sbx
> run Say.asm
wrote Say.sbx: program 46, data 93, labels 7
> load Say.sbx
> run built by the machine itself
it says: built by the machine itself
Programs That Bring Vectors:
A #Vectors line that names a handler says this program implements that vector, and the file then carries a Vector Segment: four bytes an entry, the address of the slot and the address to put in it, after the code and the data so that everything before them sits where a loader knowing nothing about vectors already expects it. A loadable program carrying any says version two, and a loader that cannot install them refuses it rather than running the program without its handlers.
#Vectors
Boot start
Device 0x00 keyHandler
Boot in a loadable program fills the entry field rather than being installed. Vector zero is where the whole machine starts, and a program being loaded into a running system has no business saying anything about that. A boot image is the one thing that does, so there it is installed like any other.
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.
Self Hosting:
It assembles CosmOS, and it assembles itself.
> load Asm.sbx
> run cosmos.asm
wrote cosmos.bin: program 7036, data 2448, labels 475
> run Asm.asm
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.
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.
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.
What It Does Not Do Yet:
Nothing in the language. Every directive this manual describes is understood, and every one of them is checked against the host assembler byte for byte on every test run.