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
127 lines
4.0 KiB
Makefile
127 lines
4.0 KiB
Makefile
# SplitBit Emulator and Assembler Makefile
|
|
# Anachronaut
|
|
# 10/16/2024
|
|
|
|
# Compiler and flags
|
|
CC ?= gcc
|
|
CFLAGS ?= -Wall -Os
|
|
PREFIX ?= /usr/local
|
|
|
|
# Have the compiler write out which headers each object depends on, so that
|
|
# editing a header rebuilds everything that includes it.
|
|
DEPFLAGS = -MMD -MP
|
|
|
|
# Both tools use POSIX interfaces that ISO C does not have: realpath, clock_gettime,
|
|
# strdup, dirname and getopt. Asking for POSIX.1-2008 by name means the build does
|
|
# not rely on the compiler happening to default to a mode where those are visible,
|
|
# and it survives someone overriding CFLAGS, which is why it is kept separate.
|
|
POSIXFLAGS = -D_POSIX_C_SOURCE=200809L
|
|
|
|
# Directories
|
|
SRC_DIR_EMU = Source/Emulator
|
|
SRC_DIR_ASM = Source/Assembler
|
|
SRC_DIR_DSK = Source/DiskTool
|
|
OBJ_DIR = Object
|
|
|
|
# Source files
|
|
EMU_SRCS = emulator.c io.c controller.c utility.c cpu.c bootstrap.c assembly.c
|
|
ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c
|
|
DSK_SRCS = SplitDisk.c
|
|
|
|
EMU_OBJS = $(EMU_SRCS:%.c=$(OBJ_DIR)/%.o)
|
|
ASM_OBJS = $(ASM_SRCS:%.c=$(OBJ_DIR)/%.o)
|
|
DSK_OBJS = $(DSK_SRCS:%.c=$(OBJ_DIR)/%.o)
|
|
|
|
# Output binary names
|
|
EMU_TARGET = SplitBit
|
|
ASM_TARGET = Assembler
|
|
DSK_TARGET = SplitDisk
|
|
|
|
# Default target: build both emulator and assembler
|
|
all: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET)
|
|
|
|
# Emulator binary
|
|
$(EMU_TARGET): $(EMU_OBJS)
|
|
$(CC) $(CFLAGS) -o $(EMU_TARGET) $(EMU_OBJS)
|
|
|
|
# Assembler binary
|
|
$(ASM_TARGET): $(ASM_OBJS)
|
|
$(CC) $(CFLAGS) -o $(ASM_TARGET) $(ASM_OBJS)
|
|
|
|
# Compile emulator source files to object files
|
|
$(OBJ_DIR)/%.o: $(SRC_DIR_EMU)/%.c
|
|
mkdir -p $(OBJ_DIR)
|
|
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
|
|
|
|
# Disk tool binary
|
|
$(DSK_TARGET): $(DSK_OBJS)
|
|
$(CC) $(CFLAGS) -o $(DSK_TARGET) $(DSK_OBJS)
|
|
|
|
# Compile disk tool source files to object files
|
|
$(OBJ_DIR)/%.o: $(SRC_DIR_DSK)/%.c
|
|
@mkdir -p $(OBJ_DIR)
|
|
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
|
|
|
|
# Compile assembler source files to object files
|
|
$(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
|
|
mkdir -p $(OBJ_DIR)
|
|
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
|
|
|
|
# Pull in the header dependencies written out by the compiler above.
|
|
-include $(EMU_OBJS:.o=.d) $(ASM_OBJS:.o=.d) $(DSK_OBJS:.o=.d)
|
|
|
|
# Run the test suite against the programs in Programs/
|
|
test: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET)
|
|
@./Tests/run.sh
|
|
@echo
|
|
@./Tests/disk.sh
|
|
@echo
|
|
@./Tests/terminal.sh
|
|
@echo
|
|
@./Tests/native.sh
|
|
@echo
|
|
@./Tests/docs.sh
|
|
|
|
# Rebuild both tools with the address and undefined behaviour sanitizers and run
|
|
# the test suite under them. Slower than 'make test', and worth running before a
|
|
# release or after anything that touches memory handling.
|
|
#
|
|
# The sanitizers catch reads and writes off the end of an array, use after free,
|
|
# leaks, and undefined arithmetic. They also fill fresh allocations with a junk
|
|
# pattern, which is what turns a read of uninitialised memory from something that
|
|
# quietly works into something the tests notice.
|
|
#
|
|
# If the suite fails, the sanitizer binaries are deliberately left in place so
|
|
# that the failing case can be run again by hand. 'make' puts the normal ones back.
|
|
SANITIZE_FLAGS = -Wall -Wextra -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer
|
|
|
|
sanitize:
|
|
@$(MAKE) --no-print-directory clean
|
|
@$(MAKE) --no-print-directory CFLAGS="$(SANITIZE_FLAGS)"
|
|
@echo "Running the test suite under AddressSanitizer and UndefinedBehaviorSanitizer."
|
|
@./Tests/run.sh
|
|
@echo
|
|
@./Tests/terminal.sh
|
|
@$(MAKE) --no-print-directory clean
|
|
@$(MAKE) --no-print-directory
|
|
@echo "Sanitizer run finished cleanly. Normal binaries rebuilt."
|
|
|
|
# Record the current output of every test as the expected result.
|
|
# Only do this when the current output is known to be correct.
|
|
bless: $(EMU_TARGET) $(ASM_TARGET)
|
|
@./Tests/run.sh --bless
|
|
|
|
# Clean up object and binary files
|
|
clean:
|
|
rm -rf $(OBJ_DIR)
|
|
rm -rf Tests/build
|
|
rm -f $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET)
|
|
|
|
# Install compiled binaries
|
|
install: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET)
|
|
mkdir -p "$(PREFIX)/bin"
|
|
install -m 755 $^ "$(PREFIX)/bin/"
|
|
|
|
# Phony targets
|
|
.PHONY: all clean install test bless sanitize
|