Various bug fixes to assembler, added more data pointers.

This commit is contained in:
Anachronaut
2026-08-13 23:41:22 -04:00
parent b50210d127
commit c2440ae5fa
38 changed files with 1028 additions and 236 deletions
+1
View File
@@ -0,0 +1 @@
Tests/build/
-8
View File
@@ -1,8 +0,0 @@
{ lib, stdenv }:
stdenv.mkDerivation {
pname = "splitbit";
version = "v1.0.0";
src = ../.;
env.PREFIX = builtins.placeholder "out";
}
+17 -1
View File
@@ -37,7 +37,9 @@ make
./SplitBit [options] [binary file] ./SplitBit [options] [binary file]
``` ```
#### Options: #### Options:
- -d, --debug: Enable debug mode to single step through cycles. - -d, --debug: Enable debug mode to single step through cycles. Each keypress advances one instruction.
- -c, --cycles N: Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time.
- -f, --fast: Run as fast as the host machine allows, ignoring the emulated cycle rate.
- -h, --help: Show help and usage information. - -h, --help: Show help and usage information.
### Usage: ### Usage:
@@ -47,6 +49,20 @@ make
#### Notes: #### 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. - 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.
### Tests:
The test suite assembles and runs every program in Programs/ and compares the results against recorded output.
```
make test
```
Tests are defined in Tests/manifest, one line per program. To record the current output as the expected result, after you have checked that it is correct:
```
make bless
```
Programs are built inside Tests/build, so running the suite never overwrites the binaries in Programs/. To run only some of the tests, call the runner directly with their names:
```
./Tests/run.sh hello 8bitFibonacci
```
### Additional Info: ### Additional Info:
For more information on the custom ISA and programming for SplitBit, see the Programming Manual and Assembler Manual. For more information on the custom ISA and programming for SplitBit, see the Programming Manual and Assembler Manual.
+95 -31
View File
@@ -9,6 +9,7 @@
#include <ctype.h> #include <ctype.h>
#include "Assm-util.h" #include "Assm-util.h"
#include "assembly.h" #include "assembly.h"
#include "../Emulator/cpu.h" // For DATA_POINTERS, so the CPU stays the one source of truth.
int debug = 0; int debug = 0;
@@ -43,42 +44,105 @@ int checkIfKeyword(intermediateElement *currentElement) {
int checkIfInstruction(intermediateElement *currentElement) { int checkIfInstruction(intermediateElement *currentElement) {
char token[32]; char token[32];
strncpy(token, currentElement->token, sizeof(token)-1); //copying over one less than the total size of the buffer ensures we wind up with a null terminated string. if (strlen(currentElement->token) >= sizeof(token)) {
toUppercase(token); // Longer than any mnemonic could be, so it is not one.
if (getOpcode(token) != 0xFE) { return 0;
// It's a valid instruction, save its value and set its type.
currentElement->type = INSTRUCTION;
currentElement->byteValue = getOpcode(token);
currentElement->byteLength = 1;
if (debug) printf("Token: %s is an instruction.\n", currentElement->token);
return 1;
} }
return 0; 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++;
}
uint8_t opcode = getOpcode(token);
if (opcode == 0xFE) {
// It's not an instruction.
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;
}
} 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);
return 1;
} }
int checkIfLiteralValue(intermediateElement *currentElement) { int checkIfLiteralValue(intermediateElement *currentElement) {
char token[32]; const char *token = currentElement->token;
strncpy(token, currentElement->token, sizeof(token)-1); if (token[0] != '0') {
if (token[0] == '0') { // It's not a literal value.
// It's a literal value. Check if it's hex or dec. return 0;
if(token[1] == 'x') {
// It's a hex literal. Set its value and type.
currentElement->type = VALUE;
currentElement->byteLength = 1;
memmove(token, token + 2, strlen(token)); // Shift the string over to get rid of the 0x.
currentElement->byteValue = (uint8_t)strtol(token, NULL, 16);
if (debug) printf("Token: %s is a hexadecimal literal. \n", currentElement->token);
} else if (token[1] == 'd') {
// It's a decimal literal. Set its value and type.
currentElement->type = VALUE;
currentElement->byteLength = 1;
memmove(token, token + 2, strlen(token)); // Shift the string over to get rid of the 0d.
currentElement->byteValue = (uint8_t)strtol(token, NULL, 10);
if (debug) printf("Token: %s is a decimal literal. \n", currentElement->token);
}
return 1;
} }
return 0; // A leading zero means the programmer was trying to write a literal, so anything
// malformed from here on is an error. Falling through to the label check instead
// would quietly emit the wrong number of bytes and shift the rest of the program.
int base;
const char *baseName;
if (token[1] == 'x') {
base = 16;
baseName = "hexadecimal";
} else if (token[1] == 'd') {
base = 10;
baseName = "decimal";
} else {
fprintf(stderr, RED "Error: Malformed literal value \"%s\".\n Literals must be prefaced with 0x for hexadecimal or 0d for decimal.\n" RESET, token);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
// Everything after the prefix has to be a digit in that base.
const char *digits = token + 2;
if (*digits == '\0') {
fprintf(stderr, RED "Error: Literal value \"%s\" has no digits after its prefix.\n" RESET, token);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
for (const char *c = digits; *c; c++) {
if (!(base == 16 ? isxdigit((unsigned char)*c) : isdigit((unsigned char)*c))) {
fprintf(stderr, RED "Error: \"%c\" is not a %s digit, in literal value \"%s\".\n" RESET, *c, baseName, token);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
}
// The digits are all valid, so the only thing left to get wrong is the range.
long value = strtol(digits, NULL, base);
if (value > 255) {
fprintf(stderr, RED "Error: Literal value \"%s\" is too large to fit in one byte.\n Values must be in the range 0x00 to 0xFF, or 0d0 to 0d255.\n" RESET, token);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
currentElement->type = VALUE;
currentElement->byteLength = 1;
currentElement->byteValue = (uint8_t)value;
if (debug) printf("Token: %s is a %s literal. \n", token, baseName);
return 1;
} }
int checkIfLabel(intermediateElement *currentElement) { int checkIfLabel(intermediateElement *currentElement) {
+1
View File
@@ -48,6 +48,7 @@ typedef struct {
char* fileName; char* fileName;
int lineNumber; int lineNumber;
uint8_t byteValue; uint8_t byteValue;
uint8_t dataPointer; // Which Data Pointer this instruction works through, if it works through one.
int byteLength; int byteLength;
uint16_t address; uint16_t address;
int type; // "KEYWORD", "INSTRUCTION" , "LABEL" , "VALUE", "STRING" int type; // "KEYWORD", "INSTRUCTION" , "LABEL" , "VALUE", "STRING"
+22
View File
@@ -82,6 +82,28 @@ const char* getMnemonic(uint8_t opcode) {
return "---"; 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.
switch (opcode) {
case 0x33: // PSHD
case 0x36: // POPD
case 0x40: // INCD
case 0x41: // DECD
case 0x42: // LDA
case 0x43: // LDB
case 0x44: // STQ
case 0x45: // STA
case 0x46: // STB
case 0x47: // SETD
case 0x48: // DPUP
case 0x49: // DPDN
return 1;
default:
return 0;
}
}
uint8_t getOpcode(char* mnemonic) { uint8_t getOpcode(char* mnemonic) {
for (int i = 0; i < num_instructions; i++) { for (int i = 0; i < num_instructions; i++) {
if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) { if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) {
+2
View File
@@ -12,4 +12,6 @@ const char* getMnemonic(uint8_t opcode);
uint8_t getOpcode(char* mnemonic); uint8_t getOpcode(char* mnemonic);
int instructionTakesDataPointer(uint8_t opcode);
#endif // CPU_H #endif // CPU_H
+64 -16
View File
@@ -13,6 +13,7 @@
#include <stdint.h> #include <stdint.h>
#include "secondPass.h" #include "secondPass.h"
#include "Assm-util.h" #include "Assm-util.h"
#include "assembly.h"
int debugSecondPass = 0; int debugSecondPass = 0;
@@ -103,6 +104,55 @@ void fillInLabelAddresses(intermediateElement *intermediateArray, int arraySize)
} }
} }
// Reports the type of the token following index i, or UNKNOWN if there isn't one.
// The operand checks go through this so that an instruction sitting at the very end of
// a program is reported as a missing operand instead of reading off the end of the array.
static int nextTokenType(intermediateElement *intermediateArray, int arraySize, int i) {
if (i + 1 >= arraySize) {
return UNKNOWN;
}
return intermediateArray[i + 1].type;
}
// Every instruction that reads operand bytes out of Program Memory needs those bytes to
// actually be there. If they aren't, the following instruction gets eaten as an operand
// and everything after it shifts, so these all have to be hard errors.
static void checkOperands(intermediateElement *intermediateArray, int arraySize, int i) {
uint8_t opcode = intermediateArray[i].byteValue;
int nextType = nextTokenType(intermediateArray, arraySize, i);
const char *problem = NULL;
if (((opcode & 0xF0) == 0x10) && (opcode != 0x1F)) {
// Branches and CALL take a two byte address, which only a label can supply.
if (nextType != LABEL) problem = "Branch without label.";
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
// The instruction is either an input or output and must be followed by a value.
if (nextType != VALUE) problem = "I/O without destination port.";
} else if (opcode == 0x26 || opcode == 0x27) {
// INIA and INIB must be followed by the literal value to load.
if (nextType != VALUE) problem = "Immediate load without a value to load.";
} else if (opcode == 0x48 || opcode == 0x49) {
// DPUP and DPDN must be followed by the literal offset to apply.
if (nextType != VALUE) problem = "Data Pointer offset without an offset value.";
} else if (opcode == 0x47) {
// SETD takes a two byte address, as either a label or a pair of literal bytes.
if (nextType == VALUE) {
if (nextTokenType(intermediateArray, arraySize, i + 1) != VALUE) {
problem = "SETD given one literal byte, but an address is two bytes.";
}
} else if (nextType != LABEL) {
problem = "SETD without an address.";
}
}
if (problem) {
fprintf(stderr, RED "Error: %s\n" RESET, problem);
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
printf("Token: %s\n", intermediateArray[i].token);
exit(1);
}
}
void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize, uint8_t *Program, int *programCount, uint8_t *Data, int *dataCount) { void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize, uint8_t *Program, int *programCount, uint8_t *Data, int *dataCount) {
for (int i = 0; i < arraySize; i++) { for (int i = 0; i < arraySize; i++) {
if (intermediateArray[i].destination == PROGRAM) { if (intermediateArray[i].destination == PROGRAM) {
@@ -110,23 +160,13 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
case INSTRUCTION: case INSTRUCTION:
// Add instruction byte to Program buffer. // Add instruction byte to Program buffer.
Program[(*programCount)++] = intermediateArray[i].byteValue; Program[(*programCount)++] = intermediateArray[i].byteValue;
// Instructions that work through a Data Pointer carry a selector
// Check if it's a branch instruction. // byte naming which one, whether or not the programmer wrote it.
if (((intermediateArray[i].byteValue & 0xF0) == 0x10) && (intermediateArray[i].byteValue != 0x1F)){ if (instructionTakesDataPointer(intermediateArray[i].byteValue)) {
if (intermediateArray[i + 1].type != LABEL) { Program[(*programCount)++] = intermediateArray[i].dataPointer;
fprintf(stderr, RED "Error: Branch without label.\n" RESET);
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
exit(1);
}
} else if ((intermediateArray[i].byteValue & 0xF0) == 0xD0 || (intermediateArray[i].byteValue & 0xF0) == 0xE0) {
// The instruction is either an input or output and must be followed by a value
if (intermediateArray[i + 1].type != VALUE) {
fprintf(stderr, RED "Error: I/O without destination port.\n" RESET);
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
printf("Token: %s\n", intermediateArray[i].token);
exit(1);
}
} }
// Make sure any operand bytes this instruction expects are present.
checkOperands(intermediateArray, arraySize, i);
break; break;
case VALUE: case VALUE:
// Add literal value to Program buffer. // Add literal value to Program buffer.
@@ -151,6 +191,14 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
} }
Data[(*dataCount)++] = '\0'; // Add null terminator to Data buffer Data[(*dataCount)++] = '\0'; // Add null terminator to Data buffer
break; 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);
break;
} }
} }
} }
+76 -43
View File
@@ -14,7 +14,10 @@ void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemor
cpu->Q = 0; cpu->Q = 0;
cpu->Status = 0; cpu->Status = 0;
cpu->ProgramCounter = 0x0000; cpu->ProgramCounter = 0x0000;
cpu->DataPointer = 0x0000; // Every Data Pointer starts at the bottom of Data Memory.
for (int i = 0; i < DATA_POINTERS; i++) {
cpu->DataPointer[i] = 0x0000;
}
cpu->StackPointer = 0xFFFF; cpu->StackPointer = 0xFFFF;
cpu->Program = programMemory; cpu->Program = programMemory;
cpu->Data = dataMemory; cpu->Data = dataMemory;
@@ -37,12 +40,16 @@ void genericCall(CPURegisters *cpu){
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (cpu->ProgramCounter >> 8) & 0xFF; cpu->Data[cpu->StackPointer] = (cpu->ProgramCounter >> 8) & 0xFF;
cpu->StackPointer--; cpu->StackPointer--;
// Push the Data Pointer to the Stack. // Push the preserved Data Pointers to the Stack, lowest numbered first.
// Order, low byte, high byte // Order within each one, low byte, high byte.
cpu->Data[cpu->StackPointer] = cpu->DataPointer & 0xFF; // The pointers above PRESERVED_DATA_POINTERS are deliberately left alone, so a
cpu->StackPointer--; // subroutine can use one to hand an address back to whoever called it.
cpu->Data[cpu->StackPointer] = (cpu->DataPointer >> 8) & 0xFF; for (int i = 0; i < PRESERVED_DATA_POINTERS; i++) {
cpu->StackPointer--; cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF;
cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF;
cpu->StackPointer--;
}
// Push B to the Stack. // Push B to the Stack.
cpu->Data[cpu->StackPointer] = cpu->B; cpu->Data[cpu->StackPointer] = cpu->B;
cpu->StackPointer--; cpu->StackPointer--;
@@ -53,6 +60,15 @@ void genericCall(CPURegisters *cpu){
genericBranch(cpu); genericBranch(cpu);
} }
uint16_t *selectDataPointer(CPURegisters *cpu) {
// Every instruction that works through a Data Pointer names which one in the
// byte immediately after the opcode. Out of range selectors are masked down
// rather than rejected, the way a narrow field in hardware would be. It is the
// assembler's job to refuse to emit one in the first place.
cpu->ProgramCounter++;
return &cpu->DataPointer[cpu->Program[cpu->ProgramCounter] & (DATA_POINTERS - 1)];
}
uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
switch(Instruction) { switch(Instruction) {
// 0x - Arithmetic and Logic Operations. // 0x - Arithmetic and Logic Operations.
@@ -164,11 +180,15 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// Pop B from the Stack. // Pop B from the Stack.
cpu->StackPointer++; cpu->StackPointer++;
cpu->B = cpu->Data[cpu->StackPointer]; cpu->B = cpu->Data[cpu->StackPointer];
// Pop the Data Pointer from the Stack. // Pop the preserved Data Pointers from the Stack. This walks the pointers
cpu->StackPointer++; // in the opposite order to genericCall, and takes the high byte before the
cpu->DataPointer = (uint16_t)cpu->Data[cpu->StackPointer] << 8; // low byte, so that it exactly mirrors the way they were pushed.
cpu->StackPointer++; for (int i = PRESERVED_DATA_POINTERS - 1; i >= 0; i--) {
cpu->DataPointer |= (uint16_t)cpu->Data[cpu->StackPointer]; cpu->StackPointer++;
cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
cpu->StackPointer++;
cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer];
}
// Pop the Return Address from the Stack. // Pop the Return Address from the Stack.
cpu->StackPointer++; cpu->StackPointer++;
cpu->ProgramCounter = (uint16_t)cpu->Data[cpu->StackPointer] << 8; cpu->ProgramCounter = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
@@ -261,14 +281,16 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->Data[cpu->StackPointer] = cpu->B; cpu->Data[cpu->StackPointer] = cpu->B;
cpu->StackPointer--; cpu->StackPointer--;
break; break;
case 0x33: case 0x33: {
// PSHD - Push the Data Pointer Address to the Stack. // PSHD - Push the selected Data Pointer Address to the Stack.
// Order, high byte, low byte // Order, high byte, low byte
// This ordering makes it easier to add offsets with register math. // This ordering makes it easier to add offsets with register math.
cpu->Data[cpu->StackPointer] = (cpu->DataPointer >> 8) & 0xFF; uint16_t pushed = *selectDataPointer(cpu);
cpu->Data[cpu->StackPointer] = (pushed >> 8) & 0xFF;
cpu->StackPointer--; cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = cpu->DataPointer & 0xFF; cpu->Data[cpu->StackPointer] = pushed & 0xFF;
cpu->StackPointer--; cpu->StackPointer--;
}
break; break;
case 0x34: case 0x34:
// POPA - Pop A from the Stack. // POPA - Pop A from the Stack.
@@ -281,62 +303,70 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->StackPointer++; cpu->StackPointer++;
cpu->B = cpu->Data[cpu->StackPointer]; cpu->B = cpu->Data[cpu->StackPointer];
break; break;
case 0x36: case 0x36: {
// POPD - Pop Data Address from the Stack. // POPD - Pop a Data Address from the Stack into the selected Data Pointer.
uint16_t *popped = selectDataPointer(cpu);
cpu->StackPointer++; cpu->StackPointer++;
cpu->DataPointer = (uint16_t)cpu->Data[cpu->StackPointer]; *popped = (uint16_t)cpu->Data[cpu->StackPointer];
cpu->StackPointer++; cpu->StackPointer++;
cpu->DataPointer |= (uint16_t)cpu->Data[cpu->StackPointer] << 8; *popped |= (uint16_t)cpu->Data[cpu->StackPointer] << 8;
}
break; break;
// //
// 4x - Data Operations: // 4x - Data Operations:
// //
case 0x40: case 0x40:
// INCD - Increment Data Pointer. // INCD - Increment the selected Data Pointer.
cpu->DataPointer++; (*selectDataPointer(cpu))++;
break; break;
case 0x41: case 0x41:
// DECD - Decrement Data Pointer. // DECD - Decrement the selected Data Pointer.
cpu->DataPointer--; (*selectDataPointer(cpu))--;
break; break;
case 0x42: case 0x42:
// LDA - Load A from Data. // LDA - Load A from Data.
cpu->A = cpu->Data[cpu->DataPointer]; cpu->A = cpu->Data[*selectDataPointer(cpu)];
break; break;
case 0x43: case 0x43:
// LDB - Load B from Data. // LDB - Load B from Data.
cpu->B = cpu->Data[cpu->DataPointer]; cpu->B = cpu->Data[*selectDataPointer(cpu)];
break; break;
case 0x44: case 0x44:
// STQ - Store Q into Data. // STQ - Store Q into Data.
cpu->Data[cpu->DataPointer] = cpu->Q; cpu->Data[*selectDataPointer(cpu)] = cpu->Q;
break; break;
case 0x45: case 0x45:
// STA - Store A into Data. // STA - Store A into Data.
cpu->Data[cpu->DataPointer] = cpu->A; cpu->Data[*selectDataPointer(cpu)] = cpu->A;
break; break;
case 0x46: case 0x46:
// STB - Store B into Data. // STB - Store B into Data.
cpu->Data[cpu->DataPointer] = cpu->B; cpu->Data[*selectDataPointer(cpu)] = cpu->B;
break; break;
case 0x47: case 0x47: {
// SETD - Set the Data Pointer. // SETD - Set the selected Data Pointer.
uint16_t *destination = selectDataPointer(cpu);
cpu->ProgramCounter++; cpu->ProgramCounter++;
uint16_t Address; uint16_t Address;
Address = (uint16_t)cpu->Program[cpu->ProgramCounter] << 8; // Cast the 8 bits to a 16 bit value and shift them to the high byte. Address = (uint16_t)cpu->Program[cpu->ProgramCounter] << 8; // Cast the 8 bits to a 16 bit value and shift them to the high byte.
cpu->ProgramCounter++; cpu->ProgramCounter++;
Address |= (uint16_t)cpu->Program[cpu->ProgramCounter]; Address |= (uint16_t)cpu->Program[cpu->ProgramCounter];
cpu-> DataPointer = Address; *destination = Address;
}
break; break;
case 0x48: case 0x48: {
// DPUP - Offset the Data Pointer up by the value of the next byte of Program Memory. // DPUP - Offset the selected Data Pointer up by the value of the next byte of Program Memory.
uint16_t *target = selectDataPointer(cpu);
cpu->ProgramCounter++; cpu->ProgramCounter++;
cpu->DataPointer += cpu->Program[cpu->ProgramCounter]; *target += cpu->Program[cpu->ProgramCounter];
}
break; break;
case 0x49: case 0x49: {
// DPDN - Offset the Data Pointer down by the value of the next byte of Program Memory. // DPDN - Offset the selected Data Pointer down by the value of the next byte of Program Memory.
uint16_t *target = selectDataPointer(cpu);
cpu->ProgramCounter++; cpu->ProgramCounter++;
cpu->DataPointer -= cpu->Program[cpu->ProgramCounter]; *target -= cpu->Program[cpu->ProgramCounter];
}
break; break;
// //
// Dx - Output Operations: // Dx - Output Operations:
@@ -383,10 +413,13 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// Unknown Instruction. // Unknown Instruction.
return 1; return 1;
} }
if (cpu->DataPointer >= cpu->StackPointer) {
// A Stack Collision was detected.
cpu->Status |= 0x82; // Set Stack Collision Flag and Halt . (Bits 7 and 1 of the Status Register);
return 2;
}
return 0; return 0;
} }
void stepCPU(CPURegisters *cpu) {
if (!(cpu->Status & 0x80)) {
// The CPU is not halted, so do a cycle.
executeOperation(cpu->Program[cpu->ProgramCounter], cpu);
cpu->ProgramCounter++;
}
}
+21 -1
View File
@@ -8,6 +8,24 @@
#include <stdint.h> #include <stdint.h>
// How many Data Pointers the CPU has. The instructions that name one take a full
// byte to do it, so the encoding would allow up to 256. The limit here is the size
// of the register file and the cost of saving pointers across a CALL, not the
// instruction format. Must be a power of two, so that the selector can be masked
// down to a valid pointer.
#define DATA_POINTERS 4
// How many Data Pointers survive a CALL. The low numbered pointers are saved and
// restored around a subroutine; the rest are left alone, so a subroutine can use
// one to hand a pointer back to its caller the way Q hands back a byte. This is
// deliberately independent of DATA_POINTERS: adding more pointers should not make
// every CALL more expensive.
#define PRESERVED_DATA_POINTERS 3
#if PRESERVED_DATA_POINTERS > DATA_POINTERS
#error "Cannot preserve more Data Pointers than the CPU has."
#endif
// The struct containing the CPU registers. // The struct containing the CPU registers.
typedef struct { typedef struct {
uint8_t A; uint8_t A;
@@ -15,7 +33,7 @@ typedef struct {
uint8_t Q; uint8_t Q;
uint8_t Status; uint8_t Status;
uint16_t ProgramCounter; uint16_t ProgramCounter;
uint16_t DataPointer; uint16_t DataPointer[DATA_POINTERS];
uint16_t StackPointer; uint16_t StackPointer;
uint8_t *Program; uint8_t *Program;
uint8_t *Data; uint8_t *Data;
@@ -25,4 +43,6 @@ uint8_t executeOperation(uint8_t instruction, CPURegisters *cpu);
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory); void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory);
void stepCPU(CPURegisters *cpu);
#endif // CPU_H #endif // CPU_H
+89 -16
View File
@@ -12,21 +12,65 @@
#include "utility.h" #include "utility.h"
#include <string.h> #include <string.h>
#include <getopt.h> #include <getopt.h>
#include <time.h>
uint8_t debugEnable = 0; // nanoseconds per second
int cycleCount = 0; #define NS_PER_SEC 1000000000LL
#define CYCLE_RATE 1000000
typedef struct {
long long cycles_per_sec; // e.g. 1000000 for 1 MHz
long long accumulator_ns; // unspent nanoseconds
struct timespec prev;
} CycleTimer;
static inline long long timespec_diff_ns(struct timespec a, struct timespec b) {
return (a.tv_sec - b.tv_sec) * NS_PER_SEC + (a.tv_nsec - b.tv_nsec);
}
void cycle_timer_init(CycleTimer *t, long long cycles_per_sec) {
t->cycles_per_sec = cycles_per_sec;
t->accumulator_ns = 0;
clock_gettime(CLOCK_MONOTONIC, &t->prev);
}
// Call once per host frame. Returns how many SplitBit cycles to execute.
int cycle_timer_tick(CycleTimer *t) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long long elapsed = timespec_diff_ns(now, t->prev);
t->prev = now;
// optional: clamp to avoid spiral-of-death on hitches
if (elapsed > NS_PER_SEC / 10) elapsed = NS_PER_SEC / 10;
t->accumulator_ns += elapsed;
long long period_ns = NS_PER_SEC / t->cycles_per_sec;
int cycles = (int)(t->accumulator_ns / period_ns);
t->accumulator_ns %= period_ns;
return cycles;
}
// How many cycles to run between glances at the wall clock. In fast mode there is
// no clock to keep pace with, so run a large batch before looking up.
#define FAST_BATCH 65536
unsigned long cycleCount = 0;
char *programFile = NULL; char *programFile = NULL;
// Memory Banks: // Memory Banks:
uint8_t Program[0x10000], Data[0x10000]; uint8_t Program[0x10000], Data[0x10000];
int main (int argc, char *argv[]) { int main (int argc, char *argv[]) {
uint8_t test = parseOptions(argc, argv); EmulatorOptions options;
if (test == 1){ uint8_t result = parseOptions(argc, argv, &options);
// Enable the Debug Mode. if (result == OPTIONS_HELP) {
debugEnable = 1; // The user asked for help and got it, which is not a failure.
} else if (test == 2){ return 0;
// User asked for help or gave a bad option, don't execute. } else if (result == OPTIONS_ERROR) {
// Bad command line, don't execute.
return 1; return 1;
} }
if (optind < argc) { if (optind < argc) {
@@ -47,18 +91,47 @@ int main (int argc, char *argv[]) {
} }
CPURegisters cpu; CPURegisters cpu;
initializeCPU(&cpu, Program, Data); initializeCPU(&cpu, Program, Data);
if(debugEnable) { if(options.debug) {
printRegisters(&cpu, Program, Data); printRegisters(&cpu, Program, Data);
} }
while (!(cpu.Status & 0x80)) {
executeOperation(cpu.Program[cpu.ProgramCounter], &cpu); CycleTimer timer;
cpu.ProgramCounter++; cycle_timer_init(&timer, CYCLE_RATE);
cycleCount++;
if (debugEnable) { uint8_t limitReached = 0;
while (!(cpu.Status & 0x80) && !limitReached) {
int cycles;
if (options.debug) {
// Debug mode advances one instruction per keypress, so the wall clock
// has no say in how many cycles to run.
cycles = 1;
} else if (options.fast) {
cycles = FAST_BATCH;
} else {
cycles = cycle_timer_tick(&timer);
}
for (int i = 0; i < cycles; i++) {
stepCPU(&cpu);
cycleCount++;
if (cpu.Status & 0x80) {
// We've halted.
break;
}
if (options.cycles && cycleCount >= options.cycles) {
limitReached = 1;
break;
}
}
if (options.debug) {
getchar(); getchar();
printRegisters(&cpu, Program, Data); printRegisters(&cpu, Program, Data);
printf("Cycle: %u\n", cycleCount); printf("Cycle: %lu\n", cycleCount);
} }
} }
printf("Execution halted after %u cycles.\n", cycleCount); if (limitReached) {
printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount);
} else {
printf("Execution halted after %lu cycles.\n", cycleCount);
}
return 0;
} }
+36 -14
View File
@@ -5,6 +5,7 @@
#include "utility.h" #include "utility.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <getopt.h> #include <getopt.h>
#include "../Assembler/assembly.h" #include "../Assembler/assembly.h"
@@ -14,43 +15,64 @@ void printHelp(const char *programName) {
printf("\n"); printf("\n");
printf("Options:\n"); printf("Options:\n");
printf(" -d, --debug Enable debug mode.\n"); printf(" -d, --debug Enable debug mode.\n");
printf(" -c, --cycles N Stop after N cycles instead of running until the program halts.\n");
printf(" -f, --fast Run as fast as possible, ignoring the emulated cycle rate.\n");
printf(" -h, --help Display this help message.\n"); printf(" -h, --help Display this help message.\n");
} }
uint8_t parseOptions(int argc, char *argv[]) { uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
static struct option long_options[] = { static struct option long_options[] = {
{"debug", no_argument, 0, 'd'}, {"debug", no_argument, 0, 'd'},
{"help", no_argument, 0, 'h'}, {"cycles", required_argument, 0, 'c'},
{0, 0, 0, 0 } {"fast", no_argument, 0, 'f'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0 }
}; };
int opt; int opt;
int option_index = 0; int option_index = 0;
options->debug = 0;
options->fast = 0;
options->cycles = 0;
// Parse options // Parse options
while ((opt = getopt_long(argc, argv, "dh", long_options, &option_index)) != -1) { while ((opt = getopt_long(argc, argv, "dc:fh", long_options, &option_index)) != -1) {
switch (opt) { switch (opt) {
case 'd': case 'd':
return 1; options->debug = 1;
break; break;
case 'c': {
// The count has to be a plain positive number. Anything else is
// almost certainly a mistyped command line rather than a request
// to run zero cycles.
char *end;
long value = strtol(optarg, &end, 10);
if (*end != '\0' || value <= 0) {
fprintf(stderr, "Error: --cycles needs a positive number, not \"%s\".\n", optarg);
return OPTIONS_ERROR;
}
options->cycles = (unsigned long)value;
}
break;
case 'f':
options->fast = 1;
break;
case 'h': case 'h':
printHelp(argv[0]); printHelp(argv[0]);
return 2; return OPTIONS_HELP;
case '?':
printHelp(argv[0]);
return 2;
default: default:
printHelp(argv[0]); printHelp(argv[0]);
return 2; return OPTIONS_ERROR;
} }
} }
return 0; return OPTIONS_OK;
} }
void printRegisters(CPURegisters *cpu, uint8_t *Program, uint8_t *Data) { void printRegisters(CPURegisters *cpu, uint8_t *Program, uint8_t *Data) {
printf("***** CPU Registers *****\n"); printf("***** CPU Registers *****\n");
printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%08b\n", cpu->A, cpu->B, cpu->Q, cpu->Status); printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%08b\n", cpu->A, cpu->B, cpu->Q, cpu->Status);
printf("Program Counter: 0x%04X Current Instruction: 0x%02X (%s)\n", cpu->ProgramCounter, Program[cpu->ProgramCounter],getMnemonic(Program[cpu->ProgramCounter])); printf("Program Counter: 0x%04X Current Instruction: 0x%02X (%s)\n", cpu->ProgramCounter, Program[cpu->ProgramCounter],getMnemonic(Program[cpu->ProgramCounter]));
printf(" Data Pointer: 0x%04X Current Data Value: 0x%02X\n", cpu->DataPointer, Data[cpu->DataPointer]); printf(" Data Pointer: 0x%04X Current Data Value: 0x%02X\n", cpu->DataPointer[0], Data[cpu->DataPointer[0]]);
printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer, Data[cpu->StackPointer+1], Data[cpu->StackPointer+2]); printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer, Data[cpu->StackPointer+1], Data[cpu->StackPointer+2]);
} }
+12 -1
View File
@@ -10,7 +10,18 @@
#include <stdint.h> #include <stdint.h>
#include "cpu.h" #include "cpu.h"
uint8_t parseOptions(int argc, char *argv[]); // Results of reading the command line.
#define OPTIONS_OK 0 // Carry on and run the program.
#define OPTIONS_HELP 1 // The user asked for help, so stop, but not because of an error.
#define OPTIONS_ERROR 2 // The command line was no good, stop and complain.
typedef struct {
uint8_t debug; // Step one instruction at a time, printing the registers.
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
} EmulatorOptions;
uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options);
void printHelp(const char *programName); void printHelp(const char *programName);
+6
View File
@@ -12,9 +12,15 @@ The assembler will accept:
- Decimal values prefaced with 0d, eg. 0d0, 0d120, 0d255. - Decimal values prefaced with 0d, eg. 0d0, 0d120, 0d255.
- Strings enclosed in double qoutes, eg. "a", "Hello, World!", "It is dark, you are likely to be eaten by a grue." - Strings enclosed in double qoutes, eg. "a", "Hello, World!", "It is dark, you are likely to be eaten by a grue."
Any token beginning with a '0' is read as a numerical literal, so a malformed one is an error rather than something the assembler tries to interpret as a label. This also means a label cannot begin with a '0'.
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 may be a string of up to 32 alphanumeric characters that must end with a colon, ':'. 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: programStart:
+16 -17
View File
@@ -3,7 +3,7 @@
SplitBit is a small 8 bit CPU. SplitBit is a small 8 bit CPU.
It is a Harvard Architecture machine with a separate 64k memory space for its Program and another for its Data. It is a Harvard Architecture machine with a separate 64k memory space for its Program and another for its Data.
It has seven registers: It has ten registers:
- The A and B Registers are each a general purpose 8 bit register. - The A and B Registers are each a general purpose 8 bit register.
- A and B are the operand registers for the ALU. - A and B are the operand registers for the ALU.
@@ -19,14 +19,13 @@ It has seven registers:
- The PC points to the current operation the CPU is executing, it initializes at Program Address 0x0000. - 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 Pointer is a 16 bit pointer into the Data Memory. - The Data Pointers (0-3) are 16 bit pointers into the Data Memory.
- The DP points to the current byte of data that the CPU can read or write to, it initializes at Data Address 0x0000. - A DP points to the current bytes of data that the CPU can read or write to, it initializes at Data Address 0x0000.
- The DP can be set arbitrarily by the programmer to any value. - A DP can be set arbitrarily by the programmer to any value.
- The Stack Pointer is a 16 bit pointer into the Data Memory. - 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 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 SP value is only modified by the push and pop instructions and cannot be set by the programmer.
- The SP must always be greater than the DP. If not, the CPU will recognize that the Stack and Data have collided and will halt.
- 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 three 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 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.
@@ -79,24 +78,24 @@ It has seven registers:
| 30 | PSHQ | Stores Q into Data Memory at the location referenced by the Stack Pointer then decrements the Stack Pointer. | | 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. | | 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. | | 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 to the stack, with the low byte on top. Increments the Stack Pointer by two. | | 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. | | 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. | | 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 from the stack, increments the Stack Pointer by two. | | 36 | POPD | Restores the Data Pointer referenced by the next byte in Program Memory from the stack, increments the Stack Pointer by two. |
### Data Operations: 10 Instructions ### Data Operations: 10 Instructions
| Hex Code | Mnemonic | Description | | Hex Code | Mnemonic | Description |
| -- | ---- | -- | | -- | ---- | -- |
| 40 | INCD | Increments the Data Pointer. | | 40 | INCD | Increments the Data Pointer referenced by the next byte in Program Memory. |
| 41 | DECD | Decrements the Data Pointer. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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 up by the value of the immediate next byte of 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 down by the value of the immediate next byte 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. |
### Output Operations: 3 Instructions ### Output Operations: 3 Instructions
+2
View File
@@ -0,0 +1,2 @@
0000 0001 0001 0002 0003 0005 0008 000D 0015 0022 0037 0059 0090 00E9 0179 0262 03DB 063D 0A18 1055 1A6D 2AC2 452F 6FF1
Execution halted after 2534 cycles.
File diff suppressed because one or more lines are too long
+225
View File
@@ -0,0 +1,225 @@
 #
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#
Execution stopped after 3000000 cycles. (cycle limit reached)
+2
View File
@@ -0,0 +1,2 @@
00000000 00000001 00000001 00000002 00000003 00000005 00000008 0000000D 00000015 00000022 00000037 00000059 00000090 000000E9 00000179 00000262 000003DB 0000063D 00000A18 00001055 00001A6D 00002AC2 0000452F 00006FF1 0000B520 00012511 0001DA31 0002FF42 0004D973 0007D8B5 000CB228 00148ADD 00213D05 0035C7E2 005704E7 008CCCC9 00E3D1B0 01709E79 02547029 03C50EA2 06197ECB 09DE8D6D 0FF80C38 19D699A5 29CEA5DD 43A53F82 6D73E55F
Execution halted after 10610 cycles.
+2
View File
@@ -0,0 +1,2 @@
0 1 1 2 3 5 8 13 21 34 55 89 144 233
Execution halted after 1506 cycles.
+2
View File
@@ -0,0 +1,2 @@
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251
Execution halted after 54061 cycles.
+2
View File
@@ -0,0 +1,2 @@
ABCZ
Execution halted after 21 cycles.
+2
View File
@@ -0,0 +1,2 @@
Hello, World!
Execution halted after 70 cycles.
+3
View File
@@ -0,0 +1,3 @@
Input Test: Will echo anything you put in.
Hello SplitBit
Execution halted after 406 cycles.
+3
View File
@@ -0,0 +1,3 @@
Input Test: Will echo anything you put in.
Hello SplitBit
Execution halted after 406 cycles.
+3
View File
@@ -0,0 +1,3 @@
00 00 00 00 01
1
Execution halted after 4707 cycles.
+2
View File
@@ -0,0 +1,2 @@
0000 0032
Execution halted after 1134 cycles.
+3
View File
@@ -0,0 +1,3 @@
Hello, World!
42 is the great answer.
Execution halted after 295 cycles.
+12
View File
@@ -0,0 +1,12 @@
Test suite for SplitBit's print.asm library.
Testing printDigit...
0 1 2 3 4 5 6 7 8 9
Testing printByteDecimal...
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
Testing printByteHex...
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF
Testing complete!
Execution halted after 71185 cycles.
+16
View File
@@ -0,0 +1,16 @@
SplitBit calculator (+ - * & | ^), Q quits.
> 0F
> 00
> 30
> 00
> FF
> 55
> 18
> Goodbye!Execution halted after 2960 cycles.
+2
View File
@@ -0,0 +1,2 @@
Two pointers, no stack shenanigans.
Execution halted after 395 cycles.
+1
View File
@@ -0,0 +1 @@
Hello SplitBit
+8
View File
@@ -0,0 +1,8 @@
0A+05
FF+01
10*03
F0&0F
0F|F0
AA^FF
20-08
Q
+65
View File
@@ -0,0 +1,65 @@
# SplitBit test manifest
# Written by Anachronaut
#
# One test per line, fields separated by '|'. Blank lines and lines starting
# with '#' are ignored.
#
# name | source | assemble-from | 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.
#
# Modes:
# run Assemble, execute, compare all output against Tests/expected/<name>.out
# assemble Assemble only. For library files that have no entry point to run
# xfail Assembly is expected to fail. Records a known breakage so that
# fixing one is noticed, and so an accidental new one is too
#
# limit is a cycle count, for programs that never halt on their own. It is passed
# to the emulator as --cycles, which bounds the run by cycles rather than by wall
# clock time and so keeps the recorded output identical from one run to the next.
# Every program is run with --fast, since the emulated cycle rate has no bearing
# 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 | - | -
# ---- The multiple Data Pointer behaviour, which nothing else exercises ----
dataPointerTest | testPrograms/dataPointerTest.asm | testPrograms | run | - | -
twoPointerCopy | testPrograms/twoPointerCopy.asm | testPrograms | 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 | -
# ---- 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
# ---- 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 | - | -
# ---- 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 | - | -
Executable
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env bash
# run.sh
# Test runner for the SplitBit Emulator and Assembler.
# Written by Anachronaut
#
# Assembles and runs every program listed in Tests/manifest and compares the
# output against the recorded results in Tests/expected.
#
# ./Tests/run.sh Run the suite.
# ./Tests/run.sh --bless Record current output as the expected results.
# ./Tests/run.sh <name>... Run only the named tests.
#
# Programs are built inside Tests/build so that running the suite never touches
# the binaries in Programs/.
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TESTS="$ROOT/Tests"
BUILD="$TESTS/build"
EXPECTED="$TESTS/expected"
INPUT="$TESTS/input"
MANIFEST="$TESTS/manifest"
ASSEMBLER="$ROOT/Assembler"
EMULATOR="$ROOT/SplitBit"
RUN_TIMEOUT=10
BLESS=0
ONLY=()
for arg in "$@"; do
case "$arg" in
--bless) BLESS=1 ;;
-h|--help)
sed -n '3,14p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
-*) echo "run.sh: unknown option $arg" >&2; exit 2 ;;
*) ONLY+=("$arg") ;;
esac
done
for tool in "$ASSEMBLER" "$EMULATOR"; do
if [ ! -x "$tool" ]; then
echo "run.sh: $tool is missing. Run 'make' first." >&2
exit 2
fi
done
rm -rf "$BUILD"
mkdir -p "$BUILD" "$EXPECTED"
cp -r "$ROOT/Programs/." "$BUILD/"
find "$BUILD" -name '*.bin' -delete
PASS=0
FAIL=0
BLESSED=0
FAILED_NAMES=()
wanted() {
[ ${#ONLY[@]} -eq 0 ] && return 0
local n
for n in "${ONLY[@]}"; do [ "$n" = "$1" ] && return 0; done
return 1
}
report() {
# report <status> <name> <detail>
printf ' [%-4s] %-20s %s\n' "$1" "$2" "$3"
}
trim() {
local v="$1"
v="${v#"${v%%[![:space:]]*}"}"
v="${v%"${v##*[![:space:]]}"}"
printf '%s' "$v"
}
check() {
# check <name> <actual-file>
local name="$1" actual="$2" golden="$EXPECTED/$1.out"
if [ "$BLESS" -eq 1 ]; then
cp "$actual" "$golden"
BLESSED=$((BLESSED + 1))
report "rec" "$name" "$(wc -c < "$golden" | tr -d ' ') bytes recorded"
return 0
fi
if [ ! -f "$golden" ]; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "no recorded output; run with --bless"
return 1
fi
if cmp -s "$actual" "$golden"; then
PASS=$((PASS + 1))
report "ok" "$name" ""
return 0
fi
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "output differs"
diff -u "$golden" "$actual" 2>/dev/null | head -20 | sed 's/^/ /'
return 1
}
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
echo "$bin"
return 0
fi
return 1
}
while IFS='|' read -r name src dir mode stdin limit; do
name="$(trim "$name")"
[ -z "$name" ] && continue
case "$name" in \#*) continue ;; esac
src="$(trim "$src")"; dir="$(trim "$dir")"
mode="$(trim "$mode")"; stdin="$(trim "$stdin")"
limit="$(trim "$limit")"
wanted "$name" || continue
if [ "$mode" = "xfail" ]; then
if assemble "$src" "$dir" >/dev/null; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "expected assembly to fail, but it succeeded"
else
PASS=$((PASS + 1))
report "ok" "$name" "fails as recorded: $(head -1 "$BUILD/.assemble.log" | tr -d '\033' | sed 's/\[[0-9;]*m//g')"
fi
continue
fi
if ! BIN="$(assemble "$src" "$dir")"; 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/^/ /'
continue
fi
if [ "$mode" = "assemble" ]; then
PASS=$((PASS + 1))
report "ok" "$name" "assembles"
continue
fi
if [ "$stdin" != "-" ] && [ ! -f "$INPUT/$stdin" ]; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "missing input fixture $stdin"
continue
fi
IN=/dev/null
[ "$stdin" != "-" ] && IN="$INPUT/$stdin"
OUT="$BUILD/.out"
case "$mode" in
run)
# --fast because there is nothing to learn from waiting out the emulated
# clock, and --cycles for programs that never halt on their own, which
# bounds them by cycle count rather than by wall clock.
EMUARGS=(--fast)
[ "$limit" != "-" ] && EMUARGS+=(--cycles "$limit")
if ! timeout "$RUN_TIMEOUT" "$EMULATOR" "${EMUARGS[@]}" "$BIN" <"$IN" >"$OUT" 2>&1; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "did not finish within ${RUN_TIMEOUT}s"
continue
fi
check "$name" "$OUT"
;;
*)
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
report "FAIL" "$name" "unknown mode '$mode' in manifest"
;;
esac
done < "$MANIFEST"
echo
if [ "$BLESS" -eq 1 ]; then
echo "Recorded $BLESSED expected results into Tests/expected."
exit 0
fi
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS tests passed."
exit 0
fi
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
exit 1
Generated
-57
View File
@@ -1,57 +0,0 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1730504689,
"narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "506278e768c2a08bec68eb62932193e341f55c90",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1730511658,
"narHash": "sha256-II0EauZdB6UiEMhTclzn3RAfo6Kr8bVrW/eL9SJKLiA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "faf7e114a7909c58ef57058b986daa5bfb08747f",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1730504152,
"narHash": "sha256-lXvH/vOfb4aGYyvFmZK/HlsNsr/0CVWlwYvo2rxJk3s=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
-28
View File
@@ -1,28 +0,0 @@
{
description = "SplitBit emulator and assembler";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
flake-parts.url = "github:hercules-ci/flake-parts";
};
outputs = inputs@{ self, nixpkgs, flake-parts }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.flake-parts.flakeModules.easyOverlay ];
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-linux" # untested
"aarch64-darwin" # untested
];
perSystem = { config, system, pkgs, ... }:
{
packages.default = pkgs.callPackage ./Nix/splitbit.nix {};
overlayAttrs.splitbit = config.packages.default;
devShells.default = pkgs.mkShell {
inputsFrom = [config.packages.default];
};
};
};
}
+20 -3
View File
@@ -7,6 +7,10 @@ CC ?= gcc
CFLAGS ?= -Wall -Os CFLAGS ?= -Wall -Os
PREFIX ?= /usr/local 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
# Directories # Directories
SRC_DIR_EMU = Source/Emulator SRC_DIR_EMU = Source/Emulator
SRC_DIR_ASM = Source/Assembler SRC_DIR_ASM = Source/Assembler
@@ -37,16 +41,29 @@ $(ASM_TARGET): $(ASM_OBJS)
# Compile emulator source files to object files # Compile emulator source files to object files
$(OBJ_DIR)/%.o: $(SRC_DIR_EMU)/%.c $(OBJ_DIR)/%.o: $(SRC_DIR_EMU)/%.c
mkdir -p $(OBJ_DIR) mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
# Compile assembler source files to object files # Compile assembler source files to object files
$(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c $(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
mkdir -p $(OBJ_DIR) mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
# Pull in the header dependencies written out by the compiler above.
-include $(EMU_OBJS:.o=.d) $(ASM_OBJS:.o=.d)
# Run the test suite against the programs in Programs/
test: $(EMU_TARGET) $(ASM_TARGET)
@./Tests/run.sh
# 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 up object and binary files
clean: clean:
rm -rf $(OBJ_DIR) rm -rf $(OBJ_DIR)
rm -rf Tests/build
rm -f $(EMU_TARGET) $(ASM_TARGET) rm -f $(EMU_TARGET) $(ASM_TARGET)
# Install compiled binaries # Install compiled binaries
@@ -55,4 +72,4 @@ install: $(EMU_TARGET) $(ASM_TARGET)
install -m 755 $^ "$(PREFIX)/bin/" install -m 755 $^ "$(PREFIX)/bin/"
# Phony targets # Phony targets
.PHONY: all clean install .PHONY: all clean install test bless