SplitBit assembles SplitBit: M1, a single file with no includes
Programs/CosmOS/Assembler/ is an assembler written in SplitBit assembly. It
runs under CosmOS, reads source off a SplitBit disk, and writes a binary back
to it with no host involved anywhere:
> run Asm.sbx hello.asm
wrote hello.bin: program 17, data 14, labels 2
THE ACCEPTANCE TEST IS THE BYTES. Tests/native.sh assembles Programs/hello.asm
both ways and compares the two files byte for byte, then runs the one the
machine built. "It ran" and "the sizes look right" both pass for a binary with
a label one byte out, which is a program that jumps into the middle of an
instruction - so the only honest test is the one SplitDisk and sbfs.asm
already work under: two implementations of one written specification, each
checking the other. The files are identical and the result prints Hello,
World! in 70 cycles.
hello.asm is the target because it is the oldest program in the repository.
The first thing this machine ever ran is now the first thing it assembles for
itself.
TWO PASSES OVER STREAMED SOURCE. The C assembler reads every token of every
file into one array; that cannot port, because cosmos.asm alone is 56,047
bytes against 64K of Data Memory. The native one streams through a 256 byte
window, twice, and keeps only the label table between the passes. Two passes
suffice because every length is known without resolving anything - an
instruction's from its shape, a value's is one, a string's is its characters
and a zero - so the first pass fixes every address 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.
The parts, each checked before anything was built on it:
source.asm characters out of a file of any size, with a line number
token.asm tokens out of characters, one character of lookahead
classify.asm what a token is, in the C assembler's order, which IS the
language: keyword, instruction, value, string, label
labels.asm names packed in an arena, four bytes of index each
numbers.asm sixteen bit arithmetic, since sbfs.asm's cannot be reached
table.asm the instruction set, generated by the same script the
monitor's copy is, and now BOTH are checked by docs.sh
readTest.asm and tokenTest.asm check the reader and the tokenizer on their
own, recorded as cosmosSource and cosmosTokens. A wrong classification does
not produce a wrong byte somewhere obvious; it produces a right looking
program of the wrong length, so it is worth catching where it happens.
WHAT IT REFUSES: #Include, #Base, #Align, #Reserve and #Vectors are refused
by name rather than ignored. Skipping a directive would produce a file that
looked right and was the wrong length, which is the worst thing an assembler
can do.
Two traps worth recording, both already known to this project and both hit
again: CALL restores A, B and DP0-DP2, so three routines returning an answer
in A had it undone by their own return; and numStep works on DP0, so three
sites that set DP1 left a pointer that never advanced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
a131a90c67
commit
dcb331c151
+16
-7
@@ -241,21 +241,30 @@ if generated.returncode != 0:
|
||||
problems.append("the instruction table generator would not run")
|
||||
else:
|
||||
wanted = [line.rstrip() for line in generated.stdout.splitlines() if line.strip()]
|
||||
monitor = read("Programs/CosmOS/Source/cosmos.asm")
|
||||
if "\nInstructions:\n" not in monitor:
|
||||
problems.append("the system has lost its instruction table")
|
||||
else:
|
||||
block = monitor.split("\nInstructions:\n")[1]
|
||||
# TWO copies now, and both are checked. The monitor has one and the assembler that
|
||||
# runs on the machine has another, because they are separate programs and there is no
|
||||
# linker to let them share: the monitor's lives in the system's data at an address
|
||||
# that moves every rebuild. Duplication is the cost of having no libraries, and a
|
||||
# check on every copy is what keeps the cost to bytes rather than to correctness.
|
||||
copies = [("the system", "Programs/CosmOS/Source/cosmos.asm", "\nInstructions:\n"),
|
||||
("the native assembler", "Programs/CosmOS/Assembler/table.asm",
|
||||
"\nAsmInstructions:\n")]
|
||||
for who, path, marker in copies:
|
||||
text = read(path)
|
||||
if marker not in text:
|
||||
problems.append("%s has lost its instruction table" % who)
|
||||
continue
|
||||
block = text.split(marker)[1]
|
||||
have = []
|
||||
for line in block.splitlines():
|
||||
if not line.strip() or not line.startswith(" 0x"):
|
||||
break
|
||||
have.append(line.rstrip())
|
||||
if have != wanted:
|
||||
problems.append("the system's instruction table is not what the assembler's"
|
||||
problems.append("%s's instruction table is not what the assembler's"
|
||||
" instruction set generates: %d entries against %d, first"
|
||||
" difference at %s"
|
||||
% (len(have), len(wanted),
|
||||
% (who, len(have), len(wanted),
|
||||
next((a or b for a, b in zip(have + [None] * len(wanted),
|
||||
wanted + [None] * len(have))
|
||||
if a != b), "the end")))
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
CosmOS
|
||||
> loaded, starting at 2000
|
||||
> ; This is a basic hello world program for the SplitBit CPU.
|
||||
; We'll create a loop that outputs each byte of our string to Output 0, the text console.
|
||||
|
||||
#Program
|
||||
|
||||
Start:
|
||||
LDA ; Load a byte of the string into A.
|
||||
BRA End ; If A is zero, branch out of the loop.
|
||||
OUTA 0x00 ; Output the value in A to Port 0, the text console.
|
||||
INCD ; Increment the Data Pointer to the next byte of the string.
|
||||
BRI Start ; Branch immediately to the start of the loop.
|
||||
|
||||
End:
|
||||
INIA 0x0A ; We'll load a linefeed into A and output it to make it look nice.
|
||||
OUTA 0x00 ; Output it to the text console.
|
||||
HALT ; Terminate the program.
|
||||
|
||||
#Data
|
||||
|
||||
"Hello, World!"
|
||||
---- lines: 21
|
||||
finished
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,25 @@
|
||||
CosmOS
|
||||
> loaded, starting at 2000
|
||||
> 4 keyword 0 [#Program]
|
||||
6 label: 0 [Start:]
|
||||
7 instr 2 [LDA]
|
||||
8 instr 1 [BRA]
|
||||
8 label 2 [End]
|
||||
9 instr 1 [OUTA]
|
||||
9 value 1 [0x00]
|
||||
10 instr 2 [INCD]
|
||||
11 instr 1 [BRI]
|
||||
11 label 2 [Start]
|
||||
13 label: 0 [End:]
|
||||
14 instr 1 [INIA]
|
||||
14 value 1 [0x0A]
|
||||
15 instr 1 [OUTA]
|
||||
15 value 1 [0x00]
|
||||
16 instr 1 [HALT]
|
||||
18 keyword 0 [#Data]
|
||||
20 string 14 [Hello, World!]
|
||||
---- no more tokens
|
||||
finished
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,3 @@
|
||||
load readTest.sbx
|
||||
run hello.asm
|
||||
exit
|
||||
@@ -0,0 +1,3 @@
|
||||
load tokenTest.sbx
|
||||
run hello.asm
|
||||
exit
|
||||
@@ -160,3 +160,21 @@ awk 'BEGIN { for (i = 0; i < 50; i++) printf "small %05d\n", i }' > small.txt
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Stream.asm" -o "$WORK/Stream.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/stream.img" "$WORK/Stream.sbx" >/dev/null
|
||||
|
||||
# A disk for the assembler that runs on the machine. It holds a source file and the two
|
||||
# programs that check the front end - the reader on its own and the tokenizer on its own -
|
||||
# because a fault in either of those would otherwise turn up much later as a mysterious
|
||||
# wrong byte in an output file.
|
||||
#
|
||||
# hello.asm is here rather than something written for the occasion because it is the
|
||||
# oldest program in the repository: the first thing this machine ever ran is the first
|
||||
# thing it assembles for itself.
|
||||
"$TOOL" format "$DISKS/asm.img" 2048 4 >/dev/null
|
||||
cp "$ROOT/Programs/hello.asm" hello.asm
|
||||
"$TOOL" put "$DISKS/asm.img" hello.asm >/dev/null
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" -I "$ROOT/Programs/CosmOS/Assembler" \
|
||||
"$ROOT/Programs/CosmOS/Assembler/readTest.asm" -o "$WORK/readTest.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/asm.img" "$WORK/readTest.sbx" >/dev/null
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" -I "$ROOT/Programs/CosmOS/Assembler" \
|
||||
"$ROOT/Programs/CosmOS/Assembler/tokenTest.asm" -o "$WORK/tokenTest.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/asm.img" "$WORK/tokenTest.sbx" >/dev/null
|
||||
|
||||
@@ -316,6 +316,18 @@ cosmosServices | CosmOS/Source/cosmos.asm | run | cosmosFil
|
||||
# notice the file moved - and that one would otherwise pass, since the blocks are still
|
||||
# there and still hold the same bytes.
|
||||
cosmosStream | CosmOS/Source/cosmos.asm | run | cosmosStream.in | - | disks/stream.img
|
||||
# The assembler's front end, checked in two pieces before anything is built on it.
|
||||
#
|
||||
# cosmosSource reads a source file and prints it back. The file crosses a block boundary,
|
||||
# so the seam is exercised rather than assumed, and the line count at the end says the
|
||||
# reader knows where it is as well as what it holds.
|
||||
#
|
||||
# cosmosTokens says what each token IS. That is the part worth checking here rather than at
|
||||
# the far end: a wrong classification does not produce a wrong byte somewhere obvious, it
|
||||
# produces a right looking program of the wrong length with everything after it shifted,
|
||||
# and by then the only symptom is a label pointing into the middle of an instruction.
|
||||
cosmosSource | CosmOS/Source/cosmos.asm | run | cosmosSource.in | - | disks/asm.img
|
||||
cosmosTokens | CosmOS/Source/cosmos.asm | run | cosmosTokens.in | - | disks/asm.img
|
||||
# The programs CosmOS loads, checked on their own so that a failure here reads as "the app
|
||||
# does not assemble" rather than as a broken disk image.
|
||||
app-greet | CosmOS/Apps/greet.asm | assemble | - | -
|
||||
@@ -328,6 +340,11 @@ app-Break | CosmOS/Apps/Break.asm | assemble | -
|
||||
app-Edit | CosmOS/Apps/Edit.asm | assemble | - | -
|
||||
app-Files | CosmOS/Apps/Files.asm | assemble | - | -
|
||||
app-Stream | CosmOS/Apps/Stream.asm | assemble | - | -
|
||||
# The assembler that runs on the machine, and its parts. Checked on their own so that a
|
||||
# failure reads as "it does not assemble" rather than as a broken disk image.
|
||||
asm-Asm | CosmOS/Assembler/Asm.asm | assemble | - | -
|
||||
asm-readTest | CosmOS/Assembler/readTest.asm | assemble | - | -
|
||||
asm-tokenTest | CosmOS/Assembler/tokenTest.asm | assemble | - | -
|
||||
|
||||
# ---- Programs driven by console input ----
|
||||
inputTest | inputTest.asm | run | inputTest.in | -
|
||||
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# Checks the assembler that runs on SplitBit against the one that runs on the host.
|
||||
#
|
||||
# THE ONLY HONEST TEST OF AN ASSEMBLER IS THE BYTES IT PRODUCES. "It ran" and "the sizes
|
||||
# look right" both pass for a binary with a label one byte out, which is a program that
|
||||
# jumps into the middle of an instruction. So this assembles the same source both ways and
|
||||
# compares the two files byte for byte, the same discipline SplitDisk and sbfs.asm work
|
||||
# under: two implementations of one written specification, each one checking the other.
|
||||
#
|
||||
# Then it runs what the machine built, because a file that matches and does not work would
|
||||
# mean both assemblers were wrong together.
|
||||
#
|
||||
# Written by Anachronaut
|
||||
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK="$ROOT/Tests/build/native"
|
||||
ASM="$ROOT/Assembler"
|
||||
TOOL="$ROOT/SplitDisk"
|
||||
EMU="$ROOT/SplitBit"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_NAMES=()
|
||||
|
||||
check() {
|
||||
local name="$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
printf ' [ok ] %-22s %s\n' "$name" "${REPORT:-}"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
printf ' [FAIL] %-22s %s\n' "$name" "${REPORT:-}"
|
||||
FAIL=$((FAIL + 1))
|
||||
FAILED_NAMES+=("$name")
|
||||
fi
|
||||
REPORT=""
|
||||
}
|
||||
|
||||
for tool in "$ASM" "$TOOL" "$EMU"; do
|
||||
[ -x "$tool" ] || { echo "$(basename "$tool") is not built."; exit 1; }
|
||||
done
|
||||
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK"
|
||||
|
||||
# The system it runs under, and the assembler itself, both built by the host assembler.
|
||||
# Bootstrapping has to start somewhere, and this is the last place it does.
|
||||
"$ASM" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
|
||||
-o "$WORK/cosmos.bin" "$ROOT/Programs/CosmOS/Source/cosmos.asm" >/dev/null || exit 1
|
||||
"$ASM" -I "$ROOT/Programs/CosmOS/Source" -I "$ROOT/Programs/CosmOS/Assembler" \
|
||||
-o "$WORK/Asm.sbx" "$ROOT/Programs/CosmOS/Assembler/Asm.asm" >/dev/null || exit 1
|
||||
|
||||
"$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" "$WORK/Asm.sbx" Asm.sbx >/dev/null
|
||||
|
||||
printf 'load Asm.sbx\nrun hello.asm\nexit\n' \
|
||||
| "$EMU" --fast --cycles 50000000 -D "$WORK/native.img" "$WORK/cosmos.bin" \
|
||||
> "$WORK/session.txt" 2>&1
|
||||
|
||||
# ---- It got as far as writing something ----
|
||||
check "it wrote a file" grep -q "wrote hello.bin" "$WORK/session.txt"
|
||||
|
||||
# ---- And the file is the one the host assembler makes ----
|
||||
"$ASM" -o "$WORK/reference.bin" "$ROOT/Programs/hello.asm" >/dev/null
|
||||
"$TOOL" get "$WORK/native.img" hello.bin "$WORK/native.bin" >/dev/null 2>&1
|
||||
if [ -f "$WORK/native.bin" ]; then
|
||||
REPORT="$(wc -c < "$WORK/native.bin" | tr -d ' ') bytes"
|
||||
fi
|
||||
check "byte for byte" cmp -s "$WORK/native.bin" "$WORK/reference.bin"
|
||||
|
||||
# ---- And what it built actually runs ----
|
||||
"$EMU" --fast --cycles 100000 "$WORK/native.bin" > "$WORK/ran.txt" 2>&1
|
||||
check "and it runs" grep -q "^Hello, World!$" "$WORK/ran.txt"
|
||||
|
||||
# ---- The report it printed says what it did ----
|
||||
REPORT="$(grep -o 'program [0-9]*, data [0-9]*, labels [0-9]*' "$WORK/session.txt" || true)"
|
||||
check "it counted right" grep -q "program 17, data 14, labels 2" "$WORK/session.txt"
|
||||
|
||||
echo
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "All $PASS native assembler checks passed."
|
||||
else
|
||||
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
|
||||
echo
|
||||
echo "The session was:"
|
||||
sed 's/^/ /' "$WORK/session.txt"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user