Long standing assembler bugs fixed, new path system. Make compatibility update.

This commit is contained in:
Anachronaut
2026-08-14 16:53:28 -04:00
parent 94b3a7af28
commit 638b68b25c
32 changed files with 1002 additions and 273 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
; A Fibonacci number generating program that uses two bytes to store the value.
#Include Libraries/print.asm
#Include print.asm
#Program
start:
+1 -1
View File
@@ -1,6 +1,6 @@
; A Fibonacci number generating program that uses four bytes to store the value.
#Include Libraries/print.asm
#Include print.asm
#Program
start:
+1 -1
View File
@@ -1,6 +1,6 @@
; A Fibonacci number generating program that uses only one byte to store the value.
#Include Libraries/print.asm
#Include print.asm
#Program
start:
+1 -1
View File
@@ -8,7 +8,7 @@
; The initial pattern is a glider. ANSI terminal control codes redraw the field
; in place. Press Ctrl-C to stop the emulator.
#Include Libraries/print.asm
#Include print.asm
#Program
+1 -1
View File
@@ -1,7 +1,7 @@
; This program asks the user for input, then prints whatever they input back to the console again.
; It stores the input string in a buffer in the Data Memory.
#Include Libraries/print.asm
#Include print.asm
#Program
+54
View File
@@ -0,0 +1,54 @@
# SplitBit Programs Makefile
# Anachronaut
#
# Builds every SplitBit program into build/, and keeps track of which libraries
# each one includes so that editing a library reassembles whatever depends on it.
#
# make Assemble everything.
# make clean Throw away build/.
# make run-hello Assemble and run one program.
ASM ?= ../Assembler
EMU ?= ../SplitBit
BUILD ?= build
# Libraries are included by bare name, so the assembler is told where to find them.
INCLUDES = -I Libraries
# The programs worth building. 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 = \
hello.asm \
printHello.asm \
inputTest.asm \
replCalculator.asm \
Fibonacci/8bitFibonacci.asm \
Fibonacci/16bitFibonacci.asm \
Fibonacci/32bitFibonacci.asm \
primeSieve/8bitSieve.asm \
primeSieve/16bitSegmentedSieve.asm \
gameOfLife/16x16Life.asm
BINARIES = $(PROGRAMS:%.asm=$(BUILD)/%.bin)
DEPENDENCIES = $(BINARIES:.bin=.d)
all: $(BINARIES)
# -M writes out which source files went into the binary, in the form of a make rule.
$(BUILD)/%.bin: %.asm
@mkdir -p $(@D)
$(ASM) $(INCLUDES) -M $(@:.bin=.d) -o $@ $<
# Assemble and run a single program, as in 'make run-hello'.
run-%: $(BUILD)/%.bin
$(EMU) $<
clean:
rm -rf $(BUILD)
# Pull in the dependency rules written by -M above, so that touching a library
# reassembles every program that includes it.
-include $(DEPENDENCIES)
.PHONY: all clean
+1 -1
View File
@@ -9,7 +9,7 @@
;
; Output is hexadecimal (0002 through FFFD), separated by spaces.
#Include Libraries/print.asm
#Include print.asm
#Program
+1 -1
View File
@@ -1,6 +1,6 @@
; This is an implementation of The Sieve of Eratosthenes that finds all the primes between 2 and 255.
#Include ../Libraries/print.asm
#Include print.asm
#Program
+1 -1
View File
@@ -5,7 +5,7 @@
; 10/27/2024
; Include the subroutine file.
#Include Libraries/print.asm
#Include print.asm
#Program
+1 -1
View File
@@ -5,7 +5,7 @@
; Whitespace is optional. Supported operators are + - * & | and ^.
; Arithmetic wraps to eight bits. Enter Q to quit.
#Include Libraries/print.asm
#Include print.asm
#Program
+1 -1
View File
@@ -1,7 +1,7 @@
; This program asks the user for input, then prints whatever they input back to the console again.
; It stores the input string in a buffer in the Data Memory.
#Include ../Libraries/print.asm
#Include print.asm
#Program
+3 -3
View File
@@ -1,8 +1,8 @@
; A test for the new math subroutines.
#Include ../Libraries/print.asm
#Include ../Libraries/int8.asm
#Include ../Libraries/int16.asm
#Include print.asm
#Include int8.asm
#Include int16.asm
#Program
start:
@@ -0,0 +1,65 @@
; Tests LDD and STD, the instructions that move a Data Pointer through Data Memory.
;
; STD writes a pointer into memory, LDD reads one back out. Together they let a
; program build and walk a table of addresses, which is the reason the CPU has
; more than one Data Pointer to walk it with.
;
; Correct output is:
; Hello
; World
; H
#Program
start:
; Build a two entry address table at runtime.
SETD.0 Slot0
SETD.1 Hello
STD.1.0 ; Write the address in DP1 to the memory addressed by DP0.
SETD.0 Slot1
SETD.1 World
STD.1.0
; Walk the table, following each entry in turn.
SETD.0 Slot0
LDD.1.0 ; DP1 becomes the address stored at DP0.
CALL printDP1
DPUP.0 0d02 ; Step DP0 over the two byte entry.
LDD.1.0
CALL printDP1
; A pointer can also follow itself, which is what LDD with one pointer means.
SETD.0 Slot0
LDD.0.0 ; DP0 becomes the address it was pointing at.
LDA.0
OUTA 0x00
INIA 0x0A
OUTA 0x00
HALT
printDP1:
; Print the string addressed by DP1. DP1 is preserved across the CALL, so the
; caller gets it back untouched.
LDA.1
BRA printDone
OUTA 0x00
INCD.1
BRI printDP1
printDone:
INIA 0x0A
OUTA 0x00
RET
#Data
Hello:
"Hello"
World:
"World"
; The table itself. Two entries, two bytes each, filled in at run time.
Slot0:
0x00 0x00
Slot1:
0x00 0x00
+1 -1
View File
@@ -1,6 +1,6 @@
; Tests for the printing subroutines provided by print.asm
#Include ../Libraries/print.asm
#Include print.asm
#Include printDigitTest.asm
#Include printDecimalTest.asm
#Include printHexTest.asm
+74
View File
@@ -0,0 +1,74 @@
; Tests address tables written down by the assembler.
;
; Naming a label in the Data Segment places its two byte address there. That is
; what lets a program lay down a table of addresses ahead of time and walk it
; with LDD, rather than having to build the table at run time with STD.
;
; The label after the table also checks that the assembler counts those two
; bytes when it works out the addresses of everything that follows.
;
; Correct output is:
; one
; two
; three
; AFTER
#Program
start:
SETD.0 Table
INIB 0d3 ; Three entries in the table.
nextEntry:
LDD.1.0 ; DP1 becomes the address held in this table slot.
CALL printDP1
DPUP.0 0d02 ; Step DP0 over the two byte slot.
DECB
BRB tableDone
BRI nextEntry
tableDone:
; Anything placed after the table has to be where the assembler said it was.
SETD.2 After
CALL printDP2
HALT
printDP1:
LDA.1
BRA print1Done
OUTA 0x00
INCD.1
BRI printDP1
print1Done:
INIA 0x0A
OUTA 0x00
RET
printDP2:
LDA.2
BRA print2Done
OUTA 0x00
INCD.2
BRI printDP2
print2Done:
INIA 0x0A
OUTA 0x00
RET
#Data
One:
"one"
Two:
"two"
Three:
"three"
; The table. Each of these names becomes the two byte address of that string.
Table:
One
Two
Three
After:
"AFTER"
+27 -2
View File
@@ -44,10 +44,35 @@ make
### Usage:
```
./Assembler [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:
- The assembled binaries are saved with the same name as the assembly source file they're assembled from, with a .bin extension, in the same directory that you call the assembler from.
- 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.
### Building Programs With Make:
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.
Programs/makefile does this for the programs in this repository:
```
cd Programs
make
```
The rule it uses is small enough to copy into your own projects:
```
$(BUILD)/%.bin: %.asm
@mkdir -p $(@D)
$(ASM) -I Libraries -M $(@:.bin=.d) -o $@ $<
-include $(BINARIES:.bin=.d)
```
### Tests:
The test suite assembles and runs every program in Programs/ and compares the results against recorded output.
+88 -6
View File
@@ -8,6 +8,8 @@
#include <stdint.h>
#include <string.h>
#include <libgen.h>
#include <unistd.h>
#include <getopt.h>
#include "Assm-util.h"
#include "firstPass.h"
#include "secondPass.h"
@@ -82,11 +84,83 @@ void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, cha
free(outputFileName);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
void printUsage(const char *programName) {
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
printf("\n");
printf("Options:\n");
printf(" -o <file> Write the binary 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(" -M <file> Write the source files this binary depends on, as a make rule.\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
// 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
// build; without them the build stops instead of just reassembling.
void writeDependencyFile(const char *dependencyPath, const char *outputPath) {
FILE *file = fopen(dependencyPath, "w");
if (!file) {
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, dependencyPath);
exit(1);
}
fprintf(file, "%s:", outputPath);
for (int i = 0; i < sourceFileCount(); i++) {
fprintf(file, " %s", sourceFile(i));
}
fprintf(file, "\n\n");
// The source itself is index 0, and always exists, so it needs no empty rule.
for (int i = 1; i < sourceFileCount(); i++) {
fprintf(file, "%s:\n", sourceFile(i));
}
fclose(file);
}
int main(int argc, char *argv[]) {
static struct option long_options[] = {
{"output", required_argument, 0, 'o'},
{"include", required_argument, 0, 'I'},
{"depend", required_argument, 0, 'M'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0 }
};
char *outputFileName = NULL;
char *dependencyFileName = NULL;
int option_index = 0;
int opt;
while ((opt = getopt_long(argc, argv, "o:I:M:h", long_options, &option_index)) != -1) {
switch (opt) {
case 'o':
outputFileName = strdup(optarg);
break;
case 'I':
addIncludeDirectory(optarg);
break;
case 'M':
dependencyFileName = strdup(optarg);
break;
case 'h':
printUsage(argv[0]);
return 0;
default:
printUsage(argv[0]);
return 1;
}
}
if (optind >= argc) {
fprintf(stderr, RED "Error: No source file specified.\n" RESET);
printUsage(argv[0]);
return 1;
}
char *fileName = argv[optind];
optind++;
if (optind < argc) {
fprintf(stderr, RED "Error: Unexpected argument: %s\n" RESET, argv[optind]);
return 1;
}
// Allocate initial space for the intermediate array.
size_t arraySize = 1024;
intermediateElement *intermediateArray = malloc(arraySize * sizeof(intermediateElement));
@@ -95,14 +169,22 @@ int main(int argc, char *argv[]) {
exit(1);
}
char *fileName = argv[1];
// The file named on the command line is the first source file, and the include
// list owns the copy that everything else points at.
char *source = recordSourceFile(strdup(fileName));
int index = 0;
loadFile(&intermediateArray, fileName, &index, &arraySize);
loadFile(&intermediateArray, source, &index, &arraySize);
populateLabelTable(intermediateArray, index);
fillInLabelAddresses(intermediateArray, index);
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
char *outputFileName = createOutputFileName(fileName);
if (!outputFileName) {
outputFileName = createOutputFileName(fileName);
}
writeOutputFile(outputFileName, Program, programLength, Data, dataLength);
if (dependencyFileName) {
writeDependencyFile(dependencyFileName, outputFileName);
free(dependencyFileName);
}
assemblerCleanup(intermediateArray, index, outputFileName);
return 0;
}
+42 -28
View File
@@ -51,13 +51,21 @@ int checkIfInstruction(intermediateElement *currentElement) {
strcpy(token, currentElement->token);
toUppercase(token);
// An instruction that works through a Data Pointer may name which one by
// hanging a selector off the mnemonic, as in LDA.2. Split that off before
// looking the mnemonic up.
char *selector = strchr(token, '.');
if (selector) {
*selector = '\0';
selector++;
// An instruction that works through a Data Pointer names which one by hanging a
// selector off the mnemonic, as in LDA.2. LDD and STD move a pointer through a
// pointer, so they take two, as in LDD.1.0. Split those off before looking the
// mnemonic up.
char *selector[MAX_DATA_POINTER_OPERANDS] = { NULL };
int selectorsGiven = 0;
char *dot = strchr(token, '.');
while (dot) {
*dot = '\0';
dot++;
if (selectorsGiven < MAX_DATA_POINTER_OPERANDS) {
selector[selectorsGiven] = dot;
}
selectorsGiven++;
dot = strchr(dot, '.');
}
uint8_t opcode = getOpcode(token);
@@ -66,32 +74,38 @@ int checkIfInstruction(intermediateElement *currentElement) {
return 0;
}
currentElement->type = INSTRUCTION;
currentElement->byteValue = opcode;
currentElement->byteLength = 1;
currentElement->dataPointer = 0;
if (instructionTakesDataPointer(opcode)) {
// The selector is emitted whether or not it was written, so these are
// always two bytes. Leaving it off is the same as writing 0.
currentElement->byteLength = 2;
if (selector) {
char *end;
long value = strtol(selector, &end, 10);
if (*selector == '\0' || *end != '\0' || value < 0 || value >= DATA_POINTERS) {
fprintf(stderr, RED "Error: \"%s\" does not name a Data Pointer.\n Selectors run from 0 to %d.\n" RESET, currentElement->token, DATA_POINTERS - 1);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
currentElement->dataPointer = (uint8_t)value;
int selectorsWanted = dataPointerOperands(opcode);
if (selectorsGiven > selectorsWanted) {
if (selectorsWanted == 0) {
fprintf(stderr, RED "Error: %s does not work through a Data Pointer, so it cannot take a selector.\n" RESET, token);
} else {
fprintf(stderr, RED "Error: %s takes %d Data Pointer selector(s), but \"%s\" gives %d.\n" RESET, token, selectorsWanted, currentElement->token, selectorsGiven);
}
} else if (selector) {
fprintf(stderr, RED "Error: %s does not work through a Data Pointer, so it cannot take a selector.\n" RESET, token);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
if (debug) printf("Token: %s is an instruction using Data Pointer %d.\n", currentElement->token, currentElement->dataPointer);
currentElement->type = INSTRUCTION;
currentElement->byteValue = opcode;
// The selectors are emitted whether or not they were written, so the length is
// fixed by the instruction. Leaving one off is the same as writing 0.
currentElement->byteLength = 1 + selectorsWanted;
for (int i = 0; i < selectorsWanted; i++) {
currentElement->dataPointer[i] = 0;
if (i < selectorsGiven && selector[i]) {
char *end;
long value = strtol(selector[i], &end, 10);
if (*selector[i] == '\0' || *end != '\0' || value < 0 || value >= DATA_POINTERS) {
fprintf(stderr, RED "Error: \"%s\" does not name a Data Pointer.\n Selectors run from 0 to %d.\n" RESET, currentElement->token, DATA_POINTERS - 1);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
currentElement->dataPointer[i] = (uint8_t)value;
}
}
if (debug) printf("Token: %s is an instruction with %d selector(s).\n", currentElement->token, selectorsWanted);
return 1;
}
+2 -1
View File
@@ -11,6 +11,7 @@
#include <string.h>
#include <ctype.h>
#include "Assm-util.h"
#include "assembly.h"
#define MAX_INCLUDES 128
@@ -48,7 +49,7 @@ typedef struct {
char* fileName;
int lineNumber;
uint8_t byteValue;
uint8_t dataPointer; // Which Data Pointer this instruction works through, if it works through one.
uint8_t dataPointer[MAX_DATA_POINTER_OPERANDS]; // Which Data Pointers this instruction works through, if any.
int byteLength;
uint16_t address;
int type; // "KEYWORD", "INSTRUCTION" , "LABEL" , "VALUE", "STRING"
+9 -3
View File
@@ -59,6 +59,8 @@ Instruction instruction_set[] = {
{0x47, "SETD"},
{0x48, "DPUP"},
{0x49, "DPDN"},
{0x4A, "LDD"},
{0x4B, "STD"},
// Output Operations:
{0xD0, "OUTQ"},
{0xD1, "OUTA"},
@@ -82,10 +84,14 @@ const char* getMnemonic(uint8_t opcode) {
return "---";
}
int instructionTakesDataPointer(uint8_t opcode) {
// These instructions all work through a Data Pointer, and so are followed by a
// byte naming which one. Everything else is a single byte opcode as before.
int dataPointerOperands(uint8_t opcode) {
// How many Data Pointer selector bytes follow this opcode. Most instructions
// have none. The ones that work through a pointer have one naming which pointer.
// LDD and STD move a pointer through a pointer, so they name two.
switch (opcode) {
case 0x4A: // LDD
case 0x4B: // STD
return 2;
case 0x33: // PSHD
case 0x36: // POPD
case 0x40: // INCD
+43 -1
View File
@@ -8,10 +8,52 @@
#ifndef ASSEMBLY_H
#define ASSEMBLY_H
// ---- The SplitBit binary format ----
//
// A binary starts with a file header, then the Program Segment, then the Data
// Segment. All multi byte numbers are stored most significant byte first.
//
// Offset Size Field
// 0 4 "SPBT", so a file that is not a SplitBit binary is spotted at once
// 4 1 Format version
// 5 4 Required feature flags
// 9 3 "PRG"
// 12 2 Program Segment length
// 14 N Program Segment
// .. 3 "DAT"
// .. 2 Data Segment length
// .. M Data Segment
//
// The feature flags are how a binary says it needs something the base machine does
// not provide, so that an emulator which cannot provide it refuses to run the binary
// 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.
#define SPLITBIT_MAGIC "SPBT"
#define SPLITBIT_MAGIC_LENGTH 4
#define SPLITBIT_FORMAT_VERSION 1
#define SPLITBIT_FLAGS_LENGTH 4
#define SEGMENT_MARKER_LENGTH 3
#define SEGMENT_LENGTH_BYTES 2
// Everything the format costs a file, on top of the two segments themselves.
#define SPLITBIT_HEADER_BYTES (SPLITBIT_MAGIC_LENGTH + 1 + SPLITBIT_FLAGS_LENGTH \
+ 2 * (SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES))
// Features this build of the emulator can provide. A binary asking for anything
// outside this set is refused.
#define SPLITBIT_FEATURES_SUPPORTED 0x00000000u
// Features the assembler currently needs to ask for. Nothing, so far.
#define SPLITBIT_FEATURES_REQUIRED 0x00000000u
const char* getMnemonic(uint8_t opcode);
uint8_t getOpcode(char* mnemonic);
int instructionTakesDataPointer(uint8_t opcode);
// How many Data Pointer selector bytes follow the given opcode. Never more than two.
#define MAX_DATA_POINTER_OPERANDS 2
int dataPointerOperands(uint8_t opcode);
#endif // CPU_H
+138 -24
View File
@@ -7,46 +7,153 @@
#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <libgen.h>
#include <limits.h>
#include "firstPass.h"
#include "Assm-util.h"
#include "assembly.h"
char *includeList[MAX_INCLUDES];
// Each source file that has gone into this assembly. The path is kept as it was
// written, because that is what reads well in an error message or a dependency
// rule. The canonical form is kept alongside it so that the same file reached by
// two different routes is recognised as one file and included only once.
typedef struct {
char *path;
char *canonical;
} SourceFile;
SourceFile includeList[MAX_INCLUDES];
int includeCount = 0;
char *includeDirectories[MAX_INCLUDES];
int includeDirectoryCount = 0;
void freeIncludeList() {
for (int i = 0; i < includeCount; i++) {
if (includeList[i]) {
free(includeList[i]);
}
free(includeList[i].path);
free(includeList[i].canonical);
}
includeCount = 0;
for (int i = 0; i < includeDirectoryCount; i++) {
free(includeDirectories[i]);
}
includeDirectoryCount = 0;
}
int isFileIncluded(const char *fileName) {
void addIncludeDirectory(const char *directory) {
if (includeDirectoryCount >= MAX_INCLUDES) {
fprintf(stderr, RED "Error: Too many include directories.\n" RESET);
exit(1);
}
includeDirectories[includeDirectoryCount] = strdup(directory);
includeDirectoryCount++;
}
int sourceFileCount() {
return includeCount;
}
const char *sourceFile(int index) {
return includeList[index].path;
}
char *recordSourceFile(char *path) {
// realpath resolves symlinks and things like "..", so that Libraries/print.asm
// and ../Libraries/print.asm are known to be the same file.
char *canonical = realpath(path, NULL);
if (!canonical) {
canonical = strdup(path);
}
for (int i = 0; i < includeCount; i++) {
if (strcmp(includeList[i], fileName) == 0) {
return 1;
if (strcmp(includeList[i].canonical, canonical) == 0) {
// Already assembled once. Including it again is not an error, it just
// does nothing, which is what lets two libraries share a dependency.
free(canonical);
free(path);
return NULL;
}
}
if (includeCount >= MAX_INCLUDES) {
fprintf(stderr, RED "Error: Too many included files.\n" RESET);
exit(1);
}
includeList[includeCount].path = path;
includeList[includeCount].canonical = canonical;
includeCount++;
return path;
}
// Joins a directory and a file name into a newly allocated path.
static char *joinPath(const char *directory, const char *name) {
if (!directory || directory[0] == '\0' || strcmp(directory, ".") == 0) {
return strdup(name);
}
size_t length = strlen(directory) + 1 + strlen(name) + 1;
char *joined = malloc(length);
if (!joined) {
fprintf(stderr, RED "Error: Memory allocation failed.\n" RESET);
exit(1);
}
snprintf(joined, length, "%s/%s", directory, name);
return joined;
}
static int fileExists(const char *path) {
FILE *file = fopen(path, "r");
if (file) {
fclose(file);
return 1;
}
return 0;
}
void addIncludedFile(const char *fileName) {
if (includeCount < MAX_INCLUDES) {
includeList[includeCount] = strdup(fileName);
includeCount++;
} else {
fprintf(stderr, RED "Error: Too many included files.\n" RESET);
exit(1);
// Works out where an #Include actually points. An absolute path is taken as given.
// Otherwise the file is looked for beside the file that asked for it, so that a
// library including its own siblings needs no help, and then along the include
// directories in the order they were given. Returns NULL if it is nowhere.
static char *resolveInclude(const char *includingFile, const char *requested) {
if (requested[0] == '/') {
return fileExists(requested) ? strdup(requested) : NULL;
}
char *copy = strdup(includingFile);
char *candidate = joinPath(dirname(copy), requested);
free(copy);
if (fileExists(candidate)) {
return candidate;
}
free(candidate);
for (int i = 0; i < includeDirectoryCount; i++) {
candidate = joinPath(includeDirectories[i], requested);
if (fileExists(candidate)) {
return candidate;
}
free(candidate);
}
return NULL;
}
// Says where the assembler looked, so that a missing include is easy to fix.
static void reportMissingInclude(const char *includingFile, const char *requested, int lineNumber) {
fprintf(stderr, RED "Error: Couldn't find included file \"%s\".\n" RESET, requested);
printf(" File: %s at line %d.\n", includingFile, lineNumber);
printf(" Looked for it:\n");
char *copy = strdup(includingFile);
printf(" beside %s\n", includingFile);
free(copy);
for (int i = 0; i < includeDirectoryCount; i++) {
printf(" in %s\n", includeDirectories[i]);
}
if (includeDirectoryCount == 0) {
printf(" and nowhere else, because no include directories were given with -I.\n");
}
}
int loadFile(intermediateElement **intermediateArray, char *fileName, int *intermediateIndex, size_t *arraySize){
// Initial setup.
addIncludedFile(fileName);
// fileName is a path already resolved and recorded by the caller, and the copy
// the include list owns, so element fileNames can safely point at it.
int status = NOWHERE;
int lineNumber = 1; // Line numbers start at 1.
// Open the file.
@@ -73,19 +180,26 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
int testValue = checkIfKeyword(&(*intermediateArray)[*intermediateIndex]);
if(testValue > 0) {
switch (testValue){
case KEYWORD_INCLUDE:
case KEYWORD_INCLUDE: {
// We need to load another file and process it before continuing.
status = NOWHERE;
// Get the filename and load up the file.
// Get the filename and work out where it actually is.
(*intermediateIndex)++;
readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber);
char *includeFile = (*intermediateArray)[*intermediateIndex].token;
if (isFileIncluded(includeFile)) {
fprintf(stderr, RED "Error: File %s is included more than once.\n" RESET, includeFile);
printf(" File: %s at line %d.\n", fileName, lineNumber);
char *requested = (*intermediateArray)[*intermediateIndex].token;
char *resolved = resolveInclude(fileName, requested);
if (!resolved) {
reportMissingInclude(fileName, requested, lineNumber);
exit(1);
}
loadFile(intermediateArray, includeFile, intermediateIndex, arraySize);
// recordSourceFile takes the path, and hands back NULL if this file
// has already been assembled. Including it twice is harmless, which
// is what lets two libraries depend on a third.
char *owned = recordSourceFile(resolved);
if (owned) {
loadFile(intermediateArray, owned, intermediateIndex, arraySize);
}
}
break;
case KEYWORD_PROGRAM:
// Set the state PROGRAM so we mark additional tokens for inclusion into Program Memory.
+12
View File
@@ -14,6 +14,18 @@
void freeIncludeList();
// Adds a directory to search when an #Include is not found beside the file that
// asked for it. Directories are searched in the order they were added.
void addIncludeDirectory(const char *directory);
// Records a source file and returns the copy the assembler will keep, which is what
// element fileNames point at. Returns NULL if this file has already been included.
char *recordSourceFile(char *path);
// Every source file that went into this assembly, for writing make dependencies.
int sourceFileCount();
const char *sourceFile(int index);
int loadFile(intermediateElement **intermediateArray, char *fileName, int *intermediateIndex, size_t *arraySize);
#endif // FIRSTPASS_H
+21 -11
View File
@@ -162,8 +162,8 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
Program[(*programCount)++] = intermediateArray[i].byteValue;
// Instructions that work through a Data Pointer carry a selector
// byte naming which one, whether or not the programmer wrote it.
if (instructionTakesDataPointer(intermediateArray[i].byteValue)) {
Program[(*programCount)++] = intermediateArray[i].dataPointer;
for (int d = 0; d < dataPointerOperands(intermediateArray[i].byteValue); d++) {
Program[(*programCount)++] = intermediateArray[i].dataPointer[d];
}
// Make sure any operand bytes this instruction expects are present.
checkOperands(intermediateArray, arraySize, i);
@@ -192,12 +192,12 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
Data[(*dataCount)++] = '\0'; // Add null terminator to Data buffer
break;
case LABEL:
// The label table reserved two bytes for this, but there's nothing here
// that knows how to emit them, so every later Data label would be shifted
// out of place. Refuse it rather than assemble something that looks fine.
fprintf(stderr, RED "Error: Label \"%s\" used as a value in the Data Segment.\n Label references are only supported in the Program Segment.\n" RESET, intermediateArray[i].token);
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
exit(1);
// A label named in the Data Segment puts its address there, which is
// how a program lays down a table of addresses for LDD to walk.
// Two bytes, most significant first, the same order addresses are
// stored in everywhere else.
Data[(*dataCount)++] = (intermediateArray[i].address >> 8) & 0xFF; // High byte
Data[(*dataCount)++] = intermediateArray[i].address & 0xFF; // Low byte
break;
}
}
@@ -211,8 +211,18 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
exit(1);
}
// 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
// features refuses the file rather than running it and going quietly wrong.
fwrite(SPLITBIT_MAGIC, sizeof(char), SPLITBIT_MAGIC_LENGTH, outputFile);
fputc(SPLITBIT_FORMAT_VERSION, outputFile);
uint32_t required = SPLITBIT_FEATURES_REQUIRED;
for (int i = SPLITBIT_FLAGS_LENGTH - 1; i >= 0; i--) {
fputc((required >> (i * 8)) & 0xFF, outputFile); // Most significant byte first.
}
// Write the "PRG" header for the program segment
fwrite("PRG", sizeof(char), 3, outputFile);
fwrite("PRG", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
// Write the program segment length as a 2-byte value (big-endian)
uint16_t programSize = programCount;
@@ -227,7 +237,7 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
}
// Write the "DAT" header for the data segment
fwrite("DAT", sizeof(char), 3, outputFile);
fwrite("DAT", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
// Write the data segment length as a 2-byte value (big-endian)
uint16_t dataSize = dataCount;
@@ -243,5 +253,5 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
fclose(outputFile);
printf("Successfully wrote SplitBit binary to \"%s\".\n", outputFileName);
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n Total size: %d bytes.\n" RESET, programCount, dataCount, (programCount+dataCount+10));
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n Total size: %d bytes.\n" RESET, programCount, dataCount, (programCount + dataCount + SPLITBIT_HEADER_BYTES));
}
+77 -57
View File
@@ -4,47 +4,80 @@
// 10/16/2024
#include "bootstrap.h"
#include "../Assembler/assembly.h" // For the binary format, which both tools share.
#include <stdio.h>
#include <string.h>
int byte = 0;
const uint8_t HEADER_LENGTH = 3; // Number of bytes for the header.
const uint8_t LENGTH_SIZE = 2; // Number of bytes for the segment length.
uint32_t readLength(FILE *file) {
uint16_t length = 0;
for (int i = 0; i < LENGTH_SIZE; i++) {
// Reads a number of the given width, most significant byte first.
// Returns 1 if the file runs out before the number does.
static uint8_t readNumber(FILE *file, int width, const char *what, uint32_t *result) {
uint32_t value = 0;
for (int i = 0; i < width; i++) {
int byte = fgetc(file);
if (byte == EOF) {
fprintf(stderr, "Error: Unexpected end of file while reading a segment length.\n");
return UINT32_MAX;
fprintf(stderr, "Error: Unexpected end of file while reading %s.\n", what);
return 1;
}
length = (length << 8) | (uint8_t)byte;
value = (value << 8) | (uint8_t)byte;
}
return length;
*result = value;
return 0;
}
uint32_t readHeader(FILE *file, const char *expectedHeader) {
char header[HEADER_LENGTH];
for (int i = 0; i < HEADER_LENGTH; i++) {
// Reads a fixed length marker and checks it against the one expected. The caller's
// buffer must have room for length + 1 characters; it always comes back with a null
// on the end, so it is safe to print in an error message either way.
static uint8_t readMarker(FILE *file, const char *expected, int length, char *found) {
memset(found, 0, length + 1);
for (int i = 0; i < length; i++) {
int byte = fgetc(file);
if (byte == EOF) {
fprintf(stderr, "Error: Unexpected end of file while reading a header.\n");
return UINT32_MAX;
return 1;
}
header[i] = (uint8_t)byte;
found[i] = (char)byte;
}
if (strncmp(header, expectedHeader, 3)) {
fprintf(stderr, "Error: Bad header.\n");
printf("Header: %s\nExpected Header: %s\n", header, expectedHeader);
return UINT32_MAX;
return strncmp(found, expected, length) != 0;
}
// Reads the file header: the magic, the format version, and the features the binary
// says it needs from the machine.
static uint8_t readFileHeader(FILE *file) {
char magic[SPLITBIT_MAGIC_LENGTH + 1];
if (readMarker(file, SPLITBIT_MAGIC, SPLITBIT_MAGIC_LENGTH, magic)) {
fprintf(stderr, "Error: This is not a SplitBit binary.\n");
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");
} else {
fprintf(stderr, " Expected the file to begin with \"%s\", found \"%s\".\n", SPLITBIT_MAGIC, magic);
}
return 1;
}
uint32_t version;
if (readNumber(file, 1, "the format version", &version)) {
return 1;
}
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);
return 1;
}
uint32_t required;
if (readNumber(file, SPLITBIT_FLAGS_LENGTH, "the feature flags", &required)) {
return 1;
}
uint32_t missing = required & ~(uint32_t)SPLITBIT_FEATURES_SUPPORTED;
if (missing) {
fprintf(stderr, "Error: This binary 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);
return 1;
}
return 0;
}
uint8_t loadSegment(FILE *file, uint8_t *Memory, uint16_t length) {
for (int16_t i = 0; i < length; i++) {
byte = fgetc(file);
static uint8_t loadSegment(FILE *file, uint8_t *Memory, uint32_t length) {
for (uint32_t i = 0; i < length; i++) {
int byte = fgetc(file);
if (byte == EOF) {
fprintf(stderr, "Error: Unexpected end of file while reading a segment.\n");
return 1;
@@ -54,44 +87,31 @@ uint8_t loadSegment(FILE *file, uint8_t *Memory, uint16_t length) {
return 0;
}
// Reads one segment: its marker, its length, and then its contents.
static uint8_t readSegment(FILE *file, const char *marker, uint8_t *Memory) {
char found[SEGMENT_MARKER_LENGTH + 1];
if (readMarker(file, marker, SEGMENT_MARKER_LENGTH, found)) {
fprintf(stderr, "Error: Expected a \"%s\" segment here, found \"%s\".\n", marker, found);
return 1;
}
uint32_t length;
if (readNumber(file, SEGMENT_LENGTH_BYTES, "a segment length", &length)) {
return 1;
}
return loadSegment(file, Memory, length);
}
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
FILE *file = fopen(path, "rb");
if (file == NULL) {
fprintf(stderr, "Error: Couldn't open file: %s\n", path);
return 1;
}
// Read the first three bytes and check if they're the PRG header.
if (readHeader(file, "PRG") == UINT32_MAX) {
fclose(file);
return 1;
}
// Now we need to get the length of the Program Section.
uint32_t length = readLength(file);
if (length == UINT32_MAX) {
fclose(file);
return 1;
}
// Now we load the Program Segment.
if (loadSegment(file, Program, (uint16_t)length)) {
fclose(file);
return 1;
}
// Okay, Program is loaded, now do the same thing but for Data.
// Check the header.
if (readHeader(file, "DAT") == UINT32_MAX) {
return 1;
}
// Get the legnth of the Data Section.
length = readLength(file);
if (length == UINT32_MAX){
fclose(file);
return 1;
}
// Now load the Data Section.
if (loadSegment(file, Data, (uint16_t)length)) {
fclose(file);
return 1;
}
// The Program Segment must come first, then the Data Segment. Short circuiting
// here means there is one exit, and so only one place that has to close the file.
uint8_t failed = readFileHeader(file)
|| readSegment(file, "PRG", Program)
|| readSegment(file, "DAT", Data);
fclose(file);
return 0;
return failed;
}
+22
View File
@@ -368,6 +368,28 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
*target -= cpu->Program[cpu->ProgramCounter];
}
break;
case 0x4A: {
// LDD - Load the first Data Pointer from the two bytes of Data Memory
// addressed by the second. Byte order matches everywhere else an address
// is stored: most significant first, then least significant.
// The source is taken by value before anything is written, so LDD.0.0
// follows the pointer in DP0 rather than tripping over itself.
uint16_t *destination = selectDataPointer(cpu);
uint16_t source = *selectDataPointer(cpu);
*destination = (uint16_t)cpu->Data[source] << 8;
*destination |= (uint16_t)cpu->Data[(uint16_t)(source + 1)];
}
break;
case 0x4B: {
// STD - Store the first Data Pointer into the two bytes of Data Memory
// addressed by the second. The cast on the second address keeps it inside
// Data Memory when the pointer sits at the very top of it.
uint16_t value = *selectDataPointer(cpu);
uint16_t address = *selectDataPointer(cpu);
cpu->Data[address] = (value >> 8) & 0xFF;
cpu->Data[(uint16_t)(address + 1)] = value & 0xFF;
}
break;
//
// Dx - Output Operations:
//
+137 -8
View File
@@ -6,6 +6,11 @@ A semicolon, ';', denotes the start of a comment, anything beyond it on a line i
Special Keywords are denoted with hash marks, '#'. The Keywords are #Include, #Program, and #Data.
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.
## 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.
@@ -14,13 +19,10 @@ The assembler will accept:
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'.
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.
## Labels:
Labels may be a string of up to 32 alphanumeric characters that must end with a colon, ':'.
A label may be referenced by name, without the colon, to place its two byte address into the Program Segment. Label references are only valid in the Program Segment.
```
programStart:
@@ -29,15 +31,108 @@ loopStart:
errorHandler01:
```
A label may be referenced by name, without the colon, to place its two byte address wherever the reference appears.
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 path to the file to include in quotes after the keyword.
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.
```
#Include string.asm
#Data
One:
"one"
Two:
"two"
Table: ; Two entries, each the two byte address of a string above.
One
Two
```
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.
## 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.
Data Pointer selectors do not count as operands here, because they are written on the mnemonic rather than after it.
## 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:
1. Beside the file that asked for it. A library including its own siblings needs no help.
2. 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 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 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. |
| -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. |
| -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.
## 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.
```
$(BUILD)/%.bin: %.asm
@mkdir -p $(@D)
$(ASM) -I Libraries -M $(@:.bin=.d) -o $@ $<
-include $(BINARIES:.bin=.d)
```
Programs/makefile in this repository builds every program that way, if you would like a longer example to copy.
## An Example SplitBit Assembly Program:
@@ -67,4 +162,38 @@ HelloString: ; By uninforced convention, Data Labels start with a capital lette
"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.
+122 -75
View File
@@ -17,105 +17,120 @@ It has ten registers:
- The Program Counter is a 16 bit pointer into the Program Memory.
- The PC points to the current operation the CPU is executing, it initializes at Program Address 0x0000.
- The PC is only modified by the branch instructions and the CALL and RET instructions. It cannot be directly set by the programmer.**
- The PC is only modified by the branch instructions and the CALL and RET instructions. It cannot be directly set by the programmer.
- The Data Pointers (0-3) are 16 bit pointers into the Data Memory.
- A DP points to the current bytes of data that the CPU can read or write to, it initializes at Data Address 0x0000.
- A DP points to a byte of data that the CPU can read or write, and each one initializes at Data Address 0x0000.
- A DP can be set arbitrarily by the programmer to any value.
- Every instruction that reads or writes Data Memory names the DP it works through. See Naming a Data Pointer below.
- Data Pointers 0, 1 and 2 are preserved through subroutine calls. Data Pointer 3 is not.
- Because DP3 is not preserved, a subroutine can use it to pass an address back to the calling routine, in the same way Q passes back a byte. Unlike Q, an address can refer to as much data as you like.
- The Stack Pointer is a 16 bit pointer into the Data Memory.
- The SP points to the current element of the stack, it initializes at location 0xFFFF.
- The SP value is only modified by the push and pop instructions and cannot be set by the programmer.
- The Stack lives in Data Memory, so a Data Pointer can be aimed at it and used to read what is on it.
- The Status register is an 8 bit register whose various bits are used as flags. Only three of these flags are used in the current implementation.
- The Status register is an 8 bit register whose various bits are used as flags. Only two of these flags are used in the current implementation.
- Bit 0 is the Carry/Borrow Flag. Any arithmetic operation either sets or clears it depending on whether or not the result causes Q to overflow/underflow. It is a 1 if a carry/underflow occurred, and a 0 otherwise. If A or B overflows or underflows from the use of an increment or decrement instruction, this flag will also be set. Non-overflowing increments or decrements will also reset it.
- Bit 1 is the Stack Collision Flag. It is set if the Data Pointer's value ever meets or exceeds the Stack Pointer's value. This condition also sets the Halt Flag.
- Bit 7 is the Halt Flag. It is set by the HALT instruction, or if there is a stack collision.
- Bit 7 is the Halt Flag. It is set by the HALT instruction.
## Naming a Data Pointer:
Twelve instructions work through a Data Pointer. Each of them carries a selector byte immediately after its opcode, naming which Data Pointer it means. LDD and STD move a pointer through a pointer, so they carry two selectors, the first naming the pointer being moved and the second naming the pointer that addresses it.
The selector is a full byte, but only enough of it is read to choose among the Data Pointers the machine has. A selector larger than the highest numbered pointer wraps around rather than being rejected, so it is the assembler's job to refuse to write one.
In assembly the selector is written on the mnemonic itself, as `LDA.2` or `LDD.1.0`. Leaving it off means Data Pointer 0, so a program that only needs one pointer never has to mention them at all. See the Assembler Manual.
## List of Instructions:
The Bytes column is the total length of the instruction, counting its opcode, any Data Pointer selectors, and any other operands it reads out of Program Memory.
### Arithmetic and Logic Operations: 9 Instructions
| Hex Code | Mnemonic | Description |
| -- | ---- | -- |
| 00 | ADD | Adds A, B, and the Carry Flag, the result is stored in Q. |
| 01 | SUB | Subtracts B and the Carry Flag from A, the result is stored in Q. |
| 02 | AND | Bitwise and of A and B, the result is stored in Q. |
| 03 | OR | Bitwise or of A and B, the result is stored in Q. |
| 04 | XOR | Bitwise xor of A and B, the result is stored in Q. |
| 05 | NOTA | Bitwise inversion of A, the result is stored in Q. |
| 06 | NOTB | Bitwise inversion of B, the result is stored in Q. |
| 07 | SHL | A and B form a circular shift register. Rotate this register left. |
| 08 | SHR | A and B form a circular shift register. Rotate this register right. |
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| 00 | ADD | 1 | Adds A, B, and the Carry Flag, the result is stored in Q. |
| 01 | SUB | 1 | Subtracts B and the Carry Flag from A, the result is stored in Q. |
| 02 | AND | 1 | Bitwise and of A and B, the result is stored in Q. |
| 03 | OR | 1 | Bitwise or of A and B, the result is stored in Q. |
| 04 | XOR | 1 | Bitwise xor of A and B, the result is stored in Q. |
| 05 | NOTA | 1 | Bitwise inversion of A, the result is stored in Q. |
| 06 | NOTB | 1 | Bitwise inversion of B, the result is stored in Q. |
| 07 | SHL | 1 | A and B form a circular shift register. Rotate this register left. |
| 08 | SHR | 1 | A and B form a circular shift register. Rotate this register right. |
### Branch and Subroutine Operations: 7 Instructions
| Hex Code | Mnemonic | Description |
| -- | ---- | -- |
| 10 | BRI | Branch Immediately. Loads the immediate next two bytes of Program Memory into the Program Counter, first the most significant byte, then the least. |
| 11 | BRQ | Branch on Q. If Q is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
| 12 | BRA | Branch on A. If A is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
| 13 | BRB | Branch on B. If B is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
| 14 | BRC | Branch if Carry is set. |
| 17 | CALL | Call subroutine. Stores all the registers to the Stack, A, B, and the Program Counter, then performs an immediate branch. |
| 1F | RET | Restores the saved registers from the Stack, then immediately branches to the Return Address by setting the Program Counter to the next instruction after the last CALL. |
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| 10 | BRI | 3 | Branch Immediately. Loads the immediate next two bytes of Program Memory into the Program Counter, first the most significant byte, then the least. |
| 11 | BRQ | 3 | Branch on Q. If Q is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
| 12 | BRA | 3 | Branch on A. If A is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
| 13 | BRB | 3 | Branch on B. If B is zero, loads the immediate next two bytes of Program Memory into the Program Counter. |
| 14 | BRC | 3 | Branch if Carry is set. |
| 17 | CALL | 3 | Call subroutine. Pushes the Program Counter, Data Pointers 0 through 2, B and A to the Stack, then performs an immediate branch. This costs ten bytes of Stack. |
| 1F | RET | 1 | Return from subroutine. Restores A, B, and Data Pointers 0 through 2 from the Stack, then sets the Program Counter to the instruction after the CALL. Data Pointer 3 and Q are left as the subroutine leaves them. |
### Register Operations: 9 Instructions
| Hex Code | Mnemonic | Description |
| -- | ---- | -- |
| 20 | RSTA | Resets A to 0. |
| 21 | RSTB | Resets B to 0. |
| 22 | INCA | Adds 1 to A. If it overflows, it sets the Carry Flag, otherwise, it resets it. |
| 23 | INCB | Adds 1 to B. If it overflows, it sets the Carry Flag, otherwise, it resets it. |
| 24 | DECA | Subtracts 1 from A. If it underflows, it sets the Carry Flag, otherwise, it resets it. |
| 25 | DECB | Subtracts 1 from B. If it underflows, it sets the Carry Flag, otherwise, it resets it. |
| 26 | INIA | Loads the next byte of Program Memory to A. |
| 27 | INIB | Loads the next byte of Program Memory to B. |
| 28 | CCF | Clears the Carry Flag. |
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| 20 | RSTA | 1 | Resets A to 0. |
| 21 | RSTB | 1 | Resets B to 0. |
| 22 | INCA | 1 | Adds 1 to A. If it overflows, it sets the Carry Flag, otherwise, it resets it. |
| 23 | INCB | 1 | Adds 1 to B. If it overflows, it sets the Carry Flag, otherwise, it resets it. |
| 24 | DECA | 1 | Subtracts 1 from A. If it underflows, it sets the Carry Flag, otherwise, it resets it. |
| 25 | DECB | 1 | Subtracts 1 from B. If it underflows, it sets the Carry Flag, otherwise, it resets it. |
| 26 | INIA | 2 | Loads the next byte of Program Memory to A. |
| 27 | INIB | 2 | Loads the next byte of Program Memory to B. |
| 28 | CCF | 1 | Clears the Carry Flag. |
### Stack Operations: 7 Instructions
| Hex Code | Mnemonic | Description |
| -- | ---- | -- |
| 30 | PSHQ | Stores Q into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
| 31 | PSHA | Stores A into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
| 32 | PSHB | Stores B into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
| 33 | PSHD | Stores the Data Pointer referenced by the next byte in Program Memory to the stack, with the low byte on top. Decrements the Stack Pointer by two. |
| 34 | POPA | Reads the location referenced by the Stack Pointer from Data Memory into A then increments the Stack Pointer. |
| 35 | POPB | Reads the location referenced by the Stack Pointer from Data Memory into B then increments the Stack Pointer. |
| 36 | POPD | Restores the Data Pointer referenced by the next byte in Program Memory from the stack, increments the Stack Pointer by two. |
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| 30 | PSHQ | 1 | Stores Q into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
| 31 | PSHA | 1 | Stores A into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
| 32 | PSHB | 1 | Stores B into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. |
| 33 | PSHD | 2 | Stores the named Data Pointer to the stack, with the low byte on top. Decrements the Stack Pointer by two. |
| 34 | POPA | 1 | Reads the location referenced by the Stack Pointer from Data Memory into A then increments the Stack Pointer. |
| 35 | POPB | 1 | Reads the location referenced by the Stack Pointer from Data Memory into B then increments the Stack Pointer. |
| 36 | POPD | 2 | Restores the named Data Pointer from the stack, increments the Stack Pointer by two. |
### Data Operations: 10 Instructions
| Hex Code | Mnemonic | Description |
| -- | ---- | -- |
| 40 | INCD | Increments the Data Pointer referenced by the next byte in Program Memory. |
| 41 | DECD | Decrements the Data Pointer referenced by the next byte in Program Memory. |
| 42 | LDA | Loads the byte referenced from Data Memory by the Data Pointer into A referenced by the next byte in Program Memory. |
| 43 | LDB | Loads the byte referenced from Data Memory by the Data Pointer into B referenced by the next byte in Program Memory. |
| 44 | STQ | Stores Q into the byte referenced by the Data Pointer in Data Memory referenced by the next byte in Program Memory. |
| 45 | STA | Stores A into the byte referenced by the Data Pointer in Data Memory referenced by the next byte in Program Memory. |
| 46 | STB | Stores B into the byte referenced by the Data Pointer in Data Memory referenced by the next byte in Program Memory. |
| 47 | SETD | Loads the next two bytes of Program Memory into the Data Pointer referenced by the next byte in Program Memory. |
| 48 | DPUP | Offset Data Pointer referenced by the next byte in Program Memory up by the value of the immediate byte after of Program Memory. |
| 49 | DPDN | Offset Data Pointer referenced by the next byte in Program Memory down by the value of the immediate byte after of Program Memory. |
### Data Operations: 12 Instructions
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| 40 | INCD | 2 | Increments the named Data Pointer. |
| 41 | DECD | 2 | Decrements the named Data Pointer. |
| 42 | LDA | 2 | Loads the byte addressed by the named Data Pointer into A. |
| 43 | LDB | 2 | Loads the byte addressed by the named Data Pointer into B. |
| 44 | STQ | 2 | Stores Q into the byte addressed by the named Data Pointer. |
| 45 | STA | 2 | Stores A into the byte addressed by the named Data Pointer. |
| 46 | STB | 2 | Stores B into the byte addressed by the named Data Pointer. |
| 47 | SETD | 4 | Loads the two bytes of Program Memory following the selector into the named Data Pointer, most significant byte first. |
| 48 | DPUP | 3 | Offsets the named Data Pointer up by the value of the byte following the selector. |
| 49 | DPDN | 3 | Offsets the named Data Pointer down by the value of the byte following the selector. |
| 4A | LDD | 3 | Loads the first named Data Pointer from the two bytes of Data Memory addressed by the second, most significant byte first. |
| 4B | STD | 3 | Stores the first named Data Pointer into the two bytes of Data Memory addressed by the second, most significant byte first. |
LDD and STD are how a program follows an address it has stored, rather than one the assembler wrote into the instruction. Together with more than one Data Pointer, they are what makes a table of addresses usable: one pointer walks the table while another follows whatever entry it is on. Naming the same pointer twice, as in `LDD.0.0`, makes that pointer follow the address it is currently holding.
### Output Operations: 3 Instructions
| Hex Code | Mnemonic | Description |
| -- | ---- | -- |
| D0 | OUTQ | Writes the value of Q to an Output specified by the next byte of Program Memory. |
| D1 | OUTA | Writes the value of A to an Output specified by the next byte of Program Memory. |
| D2 | OUTB | Writes the value of B to an Output specified by the next byte of Program Memory. |
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| D0 | OUTQ | 2 | Writes the value of Q to an Output specified by the next byte of Program Memory. |
| D1 | OUTA | 2 | Writes the value of A to an Output specified by the next byte of Program Memory. |
| D2 | OUTB | 2 | Writes the value of B to an Output specified by the next byte of Program Memory. |
### Input Operations: 2 Instructions
| Hex Code | Mnemonic | Description |
| -- | ---- | -- |
| E0 | INA | Writes the value of an Input to A. The input port is specified the next byte of Program Memory. |
| E1 | INB | Writes the value of an Input to B. The input port is specified the next byte of Program Memory. |
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| E0 | INA | 2 | Writes the value of an Input to A. The input port is specified by the next byte of Program Memory. |
| E1 | INB | 2 | Writes the value of an Input to B. The input port is specified by the next byte of Program Memory. |
### Special Operations: 2 Instructions
| Hex Code | Mnemonic | Description |
| -- | ---- | -- |
| F0 | NOP | Perform no Operation, increment the Program Counter. |
| FF | HALT | Stops CPU Execution. |
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| F0 | NOP | 1 | Perform no Operation, increment the Program Counter. |
| FF | HALT | 1 | Stops CPU Execution. |
## Input and Output In the Emulator:
@@ -145,11 +160,43 @@ End:
"Hello, World!"
```
Nothing in that program names a Data Pointer, so all of it runs through Data Pointer 0.
### Structure of a SplitBit Binary File:
The Program and Data values are both stored in a single file for loading into the system. The Program Segment must come first, then the Data Segment. The system will look for a three letter header, PRG for program and DAT for data. After the header is a two byte value representing the length of the segment. The length is stored little endian, which is typical for all values in SplitBit. The system loads the memories with the bytes from the file in sequence starting from address 0x0000.
Here's an example hex dump of the hello world program stored in the proper format:
The Program and Data values are both stored in a single file for loading into the system. Every multi byte value in the format 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 SplitBit binary 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 binary states that it needs something the base machine does not provide. An emulator that cannot provide everything a binary 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 binary.
After the header come the two segments, the Program Segment first and then the Data Segment. Each begins 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.
Here is the hello world program above, assembled and dumped as hex:
```
50 52 47 00 0F 26 12 00 0A D1 00 40 10 00 00 28 0A D1 00 FF 44 41 54 00 0E 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 00
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.
+4
View File
@@ -0,0 +1,4 @@
Hello
World
H
Execution halted after 79 cycles.
+5
View File
@@ -0,0 +1,5 @@
one
two
three
AFTER
Execution halted after 122 cycles.
+34 -31
View File
@@ -4,11 +4,10 @@
# One test per line, fields separated by '|'. Blank lines and lines starting
# with '#' are ignored.
#
# name | source | assemble-from | mode | stdin | limit
# name | source | mode | stdin | limit
#
# source and assemble-from are both relative to Programs/. assemble-from exists
# because #Include paths resolve against the current directory rather than
# against the including file, so each program has one directory it builds in.
# source is relative to Programs/. Everything assembles from there with
# Libraries/ on the include path, and the binary is written into Tests/build.
#
# Modes:
# run Assemble, execute, compare all output against Tests/expected/<name>.out
@@ -23,43 +22,47 @@
# on what a program prints.
# ---- Programs that halt on their own ----
hello | hello.asm | . | run | - | -
printHello | printHello.asm | . | run | - | -
8bitFibonacci | Fibonacci/8bitFibonacci.asm | . | run | - | -
16bitFibonacci | Fibonacci/16bitFibonacci.asm | . | run | - | -
32bitFibonacci | Fibonacci/32bitFibonacci.asm | . | run | - | -
8bitSieve | primeSieve/8bitSieve.asm | primeSieve | run | - | -
16bitSegmentedSieve | primeSieve/16bitSegmentedSieve.asm | . | run | - | -
mathTest | testPrograms/mathTest.asm | testPrograms | run | - | -
printTest | testPrograms/printTest.asm | testPrograms | run | - | -
int16print | Libraries/int16print.asm | Libraries | run | - | -
hello | hello.asm | run | - | -
printHello | printHello.asm | run | - | -
8bitFibonacci | Fibonacci/8bitFibonacci.asm | run | - | -
16bitFibonacci | Fibonacci/16bitFibonacci.asm | run | - | -
32bitFibonacci | Fibonacci/32bitFibonacci.asm | run | - | -
8bitSieve | primeSieve/8bitSieve.asm | run | - | -
16bitSegmentedSieve | primeSieve/16bitSegmentedSieve.asm | run | - | -
mathTest | testPrograms/mathTest.asm | run | - | -
printTest | testPrograms/printTest.asm | run | - | -
int16print | Libraries/int16print.asm | run | - | -
# ---- The multiple Data Pointer behaviour, which nothing else exercises ----
dataPointerTest | testPrograms/dataPointerTest.asm | testPrograms | run | - | -
twoPointerCopy | testPrograms/twoPointerCopy.asm | testPrograms | run | - | -
dataPointerTest | testPrograms/dataPointerTest.asm | run | - | -
twoPointerCopy | testPrograms/twoPointerCopy.asm | run | - | -
pointerTableTest | testPrograms/pointerTableTest.asm | run | - | -
staticTableTest | testPrograms/staticTableTest.asm | run | - | -
# ---- Programs driven by console input ----
inputTest | inputTest.asm | . | run | inputTest.in | -
inputTestOld | testPrograms/inputTest.asm | testPrograms | run | inputTest.in | -
replCalculator | replCalculator.asm | . | run | replCalculator.in | -
inputTest | inputTest.asm | run | inputTest.in | -
inputTestOld | testPrograms/inputTest.asm | run | inputTest.in | -
replCalculator | replCalculator.asm | run | replCalculator.in | -
# ---- Programs that run forever by design, bounded by a cycle count ----
# 3,000,000 cycles is about fourteen generations of the glider, which puts
# evolveBoard and its pointer juggling through its paces many times over.
16x16Life | gameOfLife/16x16Life.asm | . | run | - | 3000000
16x16Life | gameOfLife/16x16Life.asm | run | - | 3000000
# ---- Libraries: no entry point, so only check that they assemble ----
lib-int8 | Libraries/int8.asm | . | assemble | - | -
lib-int16 | Libraries/int16.asm | . | assemble | - | -
lib-int32 | Libraries/int32.asm | . | assemble | - | -
lib-math | Libraries/math.asm | . | assemble | - | -
lib-int8 | Libraries/int8.asm | assemble | - | -
lib-int16 | Libraries/int16.asm | assemble | - | -
lib-int32 | Libraries/int32.asm | assemble | - | -
lib-math | Libraries/math.asm | assemble | - | -
# ---- Known breakages, recorded rather than ignored ----
# print.asm branches to 'start', which only the including program defines.
lib-print | Libraries/print.asm | . | xfail | - | -
# These three call print.asm routines but have no #Include line at all.
printDecimalTest | testPrograms/printDecimalTest.asm | testPrograms | xfail | - | -
printDigitTest | testPrograms/printDigitTest.asm | testPrograms | xfail | - | -
printHexTest | testPrograms/printHexTest.asm | testPrograms | xfail | - | -
# shiftTest includes "print.asm", which is not beside it.
shiftTest | testPrograms/shiftTest.asm | testPrograms | xfail | - | -
lib-print | Libraries/print.asm | xfail | - | -
# These three call print.asm routines but have no #Include line at all. They are
# only ever pulled in by printTest.asm, so they are not standalone programs.
printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | -
printDigitTest | testPrograms/printDigitTest.asm | xfail | - | -
printHexTest | testPrograms/printHexTest.asm | xfail | - | -
# shiftTest calls printDecimal, which print.asm does not have. It looks like the
# routine was renamed and this caller was never updated.
shiftTest | testPrograms/shiftTest.asm | xfail | - | -
+12 -12
View File
@@ -49,10 +49,10 @@ for tool in "$ASSEMBLER" "$EMULATOR"; do
fi
done
PROGRAMS="$ROOT/Programs"
rm -rf "$BUILD"
mkdir -p "$BUILD" "$EXPECTED"
cp -r "$ROOT/Programs/." "$BUILD/"
find "$BUILD" -name '*.bin' -delete
PASS=0
FAIL=0
@@ -104,30 +104,30 @@ check() {
}
assemble() {
# assemble <source> <assemble-from>; echoes the built binary path on success
local src="$1" dir="$2"
local wd="$BUILD/$dir" rel bin
rel="$(realpath --relative-to="$wd" "$BUILD/$src")"
bin="$wd/$(basename "${src%.asm}").bin"
if ( cd "$wd" && "$ASSEMBLER" "$rel" ) >"$BUILD/.assemble.log" 2>&1; then
# assemble <name> <source>; echoes the built binary path on success.
# Everything builds from Programs/ with Libraries/ on the include path, and the
# binary goes to Tests/build, so the source tree is never written to.
local name="$1" src="$2"
local bin="$BUILD/$name.bin"
if ( cd "$PROGRAMS" && "$ASSEMBLER" -I Libraries -o "$bin" "$src" ) >"$BUILD/.assemble.log" 2>&1; then
echo "$bin"
return 0
fi
return 1
}
while IFS='|' read -r name src dir mode stdin limit; do
while IFS='|' read -r name src mode stdin limit; do
name="$(trim "$name")"
[ -z "$name" ] && continue
case "$name" in \#*) continue ;; esac
src="$(trim "$src")"; dir="$(trim "$dir")"
src="$(trim "$src")"
mode="$(trim "$mode")"; stdin="$(trim "$stdin")"
limit="$(trim "$limit")"
wanted "$name" || continue
if [ "$mode" = "xfail" ]; then
if assemble "$src" "$dir" >/dev/null; then
if assemble "$name" "$src" >/dev/null; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "expected assembly to fail, but it succeeded"
else
@@ -137,7 +137,7 @@ while IFS='|' read -r name src dir mode stdin limit; do
continue
fi
if ! BIN="$(assemble "$src" "$dir")"; then
if ! BIN="$(assemble "$name" "$src")"; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "assembly failed"
head -3 "$BUILD/.assemble.log" | tr -d '\033' | sed 's/\[[0-9;]*m//g' | sed 's/^/ /'