Files
SplitBit-Emulator/Tests/native.sh
T
AnachronautandClaude Opus 5 20989c3439 M3: programs that bring their own vectors
> load Asm.sbx
    > run Keys.asm
    wrote Keys.sbx: program 558, data 85, labels 52
    > load Keys.sbx
    > run
    keys, by interrupt. q stops.
    ab
    the console has been handed back

The machine assembles a program carrying an interrupt handler, the loader
installs its vector, the console interrupts into it, and the shell takes the
vector back at exit. Byte for byte identical to the C assembler's, and
Tests/native.sh now checks a boot image and four loadable programs on every
run.

WHAT IT TOOK:

  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. The name is met twice and
  the second time fills in the handler, which is what lets one shared file
  serve both the caller and the implementer.

  So the first pass declares and the second implements. That is forced: a
  handler is an address, and no address is known until every label has been
  placed.

  Boot in a loadable program fills the entry field rather than being
  installed - vector zero is where the whole machine starts, and a program
  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, behind a "VEC" marker in the SPBT file.

  Device is named by the port, and Device with the five reserved names are
  matched without regard to case, the way mnemonics are: they are part of
  the language rather than names the programmer chose. Devices have no names
  of their own, so they are given one nothing can type.

TWO BUGS, both of a kind worth naming.

The first: "is this a loadable program" was written out as an OR of the two
segment bases in seven places, and the sense wanted is the opposite in most
of them. One of the seven had it backwards and put a version ONE header on a
file carrying vectors, which a loader is right to refuse. It is one flag
now, settled once and tested the same way everywhere.

The second: finding the entry to write a handler into means calling vecFind,
which reads the entry's fields out - including the handler it does not have
yet. An address resolved into VecHandler before the find was overwritten
with zero by the find itself, and the file came out with a vector pointing
at address zero: a slot that looked installed and went nowhere. The
resolved address has a variable of its own now.

Keys.asm and console.asm go on the CosmOS disk, so the whole path can be
watched rather than only tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-21 11:50:52 -04:00

130 lines
5.3 KiB
Bash
Executable File

#!/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.
#
# It checks a boot image and four loadable programs. The loadable ones are the harder case:
# they include another file, they are based somewhere other than zero, and every service
# they call is a name declared in that included file. The last of them also brings a vector,
# which is the only thing here that asks anything of its loader.
#
# 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
# The applications, and the file of service names they all include. These are the reason
# the second milestone exists: an assembler that cannot follow an #Include cannot build
# anything that asks the system for anything.
#
# Keys is the last of them and the hardest: it brings a vector of its own, so it needs a
# Vector Segment in the output and the version two header that says so. Nothing else here
# asks anything of its loader beyond code and data.
APPS="Say greet Files Keys"
"$TOOL" put "$WORK/native.img" "$ROOT/Programs/CosmOS/Source/services.asm" services.asm >/dev/null
"$TOOL" put "$WORK/native.img" "$ROOT/Programs/CosmOS/Source/console.asm" console.asm >/dev/null
for app in $APPS; do
"$TOOL" put "$WORK/native.img" "$ROOT/Programs/CosmOS/Apps/$app.asm" "$app.asm" >/dev/null
done
{
echo "load Asm.sbx"
echo "run hello.asm"
for app in $APPS; do echo "run $app.asm"; done
echo "exit"
} | "$EMU" --fast --cycles 600000000 -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" | head -1 || true)"
check "it counted right" grep -q "program 17, data 14, labels 2" "$WORK/session.txt"
# ---- And the applications, which need an include, a base and the service names ----
#
# A loadable program is the harder case and the one that matters: #Include splices another
# file in, #Base moves every label to where the program will really live, and every SWI in
# them names a vector declared in a file the source never mentions by number.
for app in $APPS; do
"$ASM" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
-o "$WORK/ref-$app.sbx" "$ROOT/Programs/CosmOS/Apps/$app.asm" >/dev/null
rm -f "$WORK/got-$app.sbx"
"$TOOL" get "$WORK/native.img" "$app.sbx" "$WORK/got-$app.sbx" >/dev/null 2>&1
if [ -f "$WORK/got-$app.sbx" ]; then
REPORT="$(wc -c < "$WORK/got-$app.sbx" | tr -d ' ') bytes"
fi
check "$app byte for byte" cmp -s "$WORK/got-$app.sbx" "$WORK/ref-$app.sbx"
done
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