The quotes are gone by the time a token is classified, so checkIfKeyword's test of token[0] == '#' matched the STRING "#Program" sitting in a program's Data Segment. It was read as the directive: the segment silently changed in the middle of the data, the string's nine bytes were charged to the Program cursor instead of the Data one, and every label defined after it came out nine bytes wrong - in a file that still had a valid header, a plausible length, and nothing to say about any of it. The only symptom was a program that jumped into the middle of an instruction. This is the FOURTH of the family. A string spelling a mnemonic assembled as that instruction; a string beginning with a zero was rejected as a malformed literal; a string in the Program Segment was discarded in silence. The instruction check and the literal check both carry a "not a STRING" guard already. This one did not, so it has one now, and it lives inside checkIfKeyword rather than at the call site so it cannot be left off again. Nothing had ever triggered it, because nothing had ever needed a directive's name as data. An assembler written FOR this machine necessarily does: it has to compare tokens against "#Program" and "#Data". It was found by building one and watching it fault on its second instruction. Test stringKeyword puts every directive name in a program's data and prints a label defined after them. Verified that it bites: without the fix the assembler refuses the file outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
365 lines
15 KiB
C
365 lines
15 KiB
C
// Assm-util.c
|
|
// Utility and helper functions for the SplitBit Assembler.
|
|
// Written by Anachronaut
|
|
// 10/25/2024
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <ctype.h>
|
|
#include "Assm-util.h"
|
|
#include "assembly.h"
|
|
#include "../Emulator/cpu.h" // For DATA_POINTERS, so the CPU stays the one source of truth.
|
|
|
|
int debug = 0;
|
|
|
|
static uint16_t segmentBases[3];
|
|
static int basesGiven = 0;
|
|
// Per segment, because a base of zero that was asked for and a base of zero that was
|
|
// never mentioned are different things, and only the second one is likely a mistake.
|
|
static int baseGiven[3];
|
|
|
|
void setSegmentBase(int segment, uint16_t base) {
|
|
segmentBases[segment] = base;
|
|
baseGiven[segment] = 1;
|
|
basesGiven = 1;
|
|
}
|
|
|
|
uint16_t segmentBase(int segment) {
|
|
return segmentBases[segment];
|
|
}
|
|
|
|
int segmentBaseWasGiven(int segment) {
|
|
return baseGiven[segment];
|
|
}
|
|
|
|
int programIsLoadable(void) {
|
|
return basesGiven;
|
|
}
|
|
|
|
void toUppercase(char *str) {
|
|
for (int i = 0; str[i]; i++) {
|
|
str[i] = toupper(str[i]);
|
|
}
|
|
}
|
|
|
|
int checkIfKeyword(intermediateElement *currentElement) {
|
|
// A string is never a keyword, however it is spelled. The quotes are gone by the time
|
|
// a token is looked at, so a program with "#Program" in its data - which is exactly
|
|
// what an assembler written for this machine needs, to compare tokens against - had
|
|
// the string read as the directive. The segment silently changed in the middle of the
|
|
// Data Segment, the string's nine bytes were charged to the Program cursor, and every
|
|
// label after it was nine bytes out while the file itself still looked well formed.
|
|
//
|
|
// This is the fourth of this family: the instruction check and the literal check both
|
|
// carry the same guard, for the same reason. It lives inside this one rather than at
|
|
// the call site so that it cannot be left off again.
|
|
if (currentElement->type == STRING) {
|
|
return 0;
|
|
}
|
|
if (currentElement->token[0] == '#') {
|
|
if (debug) printf("Token: %s is a keyword.\n", currentElement->token);
|
|
currentElement->type = KEYWORD;
|
|
currentElement->destination = NOWHERE;
|
|
if (strcmp(currentElement->token, "#Include") == 0) {
|
|
return KEYWORD_INCLUDE;
|
|
} else if (strcmp(currentElement->token, "#Program") == 0) {
|
|
// We'll want to remember we encountered this and mark all the additional tokens until we hit another keyword for inclusion in the Program Segment.
|
|
return KEYWORD_PROGRAM;
|
|
} else if (strcmp(currentElement->token, "#Data") == 0) {
|
|
// Same as for #Program, but mark for inclusion in the Data Segment.
|
|
return KEYWORD_DATA;
|
|
} else if (strcmp(currentElement->token, "#Align") == 0) {
|
|
// Puts down as many zero bytes as it takes to reach the next multiple of
|
|
// the number that follows. The file that needs the boundary is then the
|
|
// file that asks for it, rather than relying on whatever came before.
|
|
return KEYWORD_ALIGN;
|
|
} else if (strcmp(currentElement->token, "#Base") == 0) {
|
|
// Says where this segment is loaded, which makes the program a loadable one
|
|
// rather than a boot image.
|
|
return KEYWORD_BASE;
|
|
} else if (strcmp(currentElement->token, "#Reserve") == 0) {
|
|
// Puts down the number of zero bytes that follows, so that a label can
|
|
// stand for a region rather than just its first byte.
|
|
return KEYWORD_RESERVE;
|
|
} else if (strcmp(currentElement->token, "#Vectors") == 0) {
|
|
// Names which handler belongs to which vector. Nothing here is assembled
|
|
// into either segment; it is worked out and written into the vector table.
|
|
return KEYWORD_VECTORS;
|
|
} else {
|
|
// It's a malformed keyword.
|
|
fprintf(stderr, RED "Error: Invalid Keyword \"%s\" in file \"%s\" at line number %d.\n" RESET, currentElement->token, currentElement->fileName, currentElement->lineNumber);
|
|
exit(1);
|
|
}
|
|
}
|
|
// It's not a keyword.
|
|
return 0;
|
|
}
|
|
|
|
int checkIfInstruction(intermediateElement *currentElement) {
|
|
char token[32];
|
|
if (strlen(currentElement->token) >= sizeof(token)) {
|
|
// Longer than any mnemonic could be, so it is not one.
|
|
return 0;
|
|
}
|
|
strcpy(token, currentElement->token);
|
|
toUppercase(token);
|
|
|
|
// 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);
|
|
if (opcode == 0xFE) {
|
|
// It's not an instruction.
|
|
return 0;
|
|
}
|
|
|
|
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);
|
|
}
|
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
|
exit(1);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
int checkIfLiteralValue(intermediateElement *currentElement) {
|
|
const char *token = currentElement->token;
|
|
if (token[0] != '0') {
|
|
// It's not a literal value.
|
|
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;
|
|
}
|
|
|
|
// Shared by readCount and readAddress, which differ only in whether zero is an answer.
|
|
// A count of nothing is a typo; an address of zero is the bottom of memory.
|
|
static uint16_t readNumber(intermediateElement *currentElement, const char *what, long least) {
|
|
const char *token = currentElement->token;
|
|
int base;
|
|
const char *baseName;
|
|
if (token[0] != '0' || (token[1] != 'x' && token[1] != 'd')) {
|
|
fprintf(stderr, RED "Error: %s needs a number, prefaced with 0x or 0d. Found \"%s\".\n" RESET, what, token);
|
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
|
exit(1);
|
|
}
|
|
if (token[1] == 'x') {
|
|
base = 16;
|
|
baseName = "hexadecimal";
|
|
} else {
|
|
base = 10;
|
|
baseName = "decimal";
|
|
}
|
|
const char *digits = token + 2;
|
|
if (*digits == '\0') {
|
|
fprintf(stderr, RED "Error: %s was given \"%s\", which has no digits after its prefix.\n" RESET, what, 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 \"%s\".\n" RESET, *c, baseName, token);
|
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
|
exit(1);
|
|
}
|
|
}
|
|
long value = strtol(digits, NULL, base);
|
|
if (value < least || value > 0xFFFF) {
|
|
fprintf(stderr, RED "Error: %s was given \"%s\". It has to be at least %ld and no more than 0xFFFF.\n" RESET, what, token, least);
|
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
|
exit(1);
|
|
}
|
|
return (uint16_t)value;
|
|
}
|
|
|
|
uint16_t readCount(intermediateElement *currentElement, const char *what) {
|
|
return readNumber(currentElement, what, 1);
|
|
}
|
|
|
|
uint16_t readAddress(intermediateElement *currentElement, const char *what) {
|
|
return readNumber(currentElement, what, 0);
|
|
}
|
|
|
|
int checkIfLabel(intermediateElement *currentElement) {
|
|
char *token = currentElement->token;
|
|
int length = strlen(token);
|
|
// Check if the last character is a colon.
|
|
if (token[length - 1] == ':') {
|
|
// It is, so this is a label definition.
|
|
currentElement->type = LABEL_DEFINITION;
|
|
currentElement->destination = NOWHERE;
|
|
if (debug) printf("Token: %s is a label definition.\n", token);
|
|
return 1;
|
|
} else {
|
|
// By process of elimination, if whatever this is wasn't picked up by any of the other checks, it's either a label or is invalid.
|
|
currentElement->type = LABEL;
|
|
currentElement->byteLength = 2;
|
|
if (debug) printf("Token: %s is probably a label?\n", token);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber) {
|
|
int c;
|
|
char buffer[256]; // Buffer to hold the token temporarily.
|
|
int i = 0;
|
|
|
|
// Step 1: Skip whitespace and comments.
|
|
while ((c = fgetc(file)) != EOF) {
|
|
if (isspace(c)) {
|
|
if (c == '\n') (*lineNumber)++; // Increment line count on newlines.
|
|
continue; // Skip whitespace
|
|
} else if (c == ';') {
|
|
// A comment: ignore characters until the newline.
|
|
while ((c = fgetc(file)) != EOF && c != '\n');
|
|
if (c == '\n') (*lineNumber)++; // Increment line count after comment line.
|
|
continue;
|
|
} else {
|
|
break; // Found a non-whitespace, non-comment character
|
|
}
|
|
}
|
|
|
|
// Step 2: Handle EOF
|
|
if (c == EOF) {
|
|
return 0; // Indicate end of file
|
|
}
|
|
|
|
// Step 3: Handle string literals
|
|
if (c == '"') {
|
|
while ((c = fgetc(file)) != EOF && c != '"') {
|
|
if (i < (int)(sizeof(buffer) - 1)) {
|
|
buffer[i++] = c;
|
|
} else {
|
|
// Say the limit and where it was met. A string long enough to reach this
|
|
// is usually several lines of help text, and "too long" on its own leaves
|
|
// somebody counting characters to find out by how much.
|
|
fprintf(stderr, RED "Error: String literal longer than %d characters.\n"
|
|
" Every string carries its own zero byte, so two written in a row"
|
|
" are two strings\n rather than one long one. Give each its own"
|
|
" label and print them one after another.\n" RESET,
|
|
(int)(sizeof(buffer) - 1));
|
|
printf(" File: %s at line %d.\n", currentElement->fileName, *lineNumber);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
buffer[i] = '\0'; // Null-terminate the string
|
|
|
|
// Store in intermediateElement and set type
|
|
currentElement->token = strdup(buffer);
|
|
currentElement->type = STRING;
|
|
currentElement->byteLength = strlen(currentElement->token)+1;
|
|
if (debug) printf("Token: %s is a string literal.\n", currentElement->token);
|
|
return 1; // Success
|
|
}
|
|
|
|
// Step 4: Handle non-string tokens
|
|
ungetc(c, file); // Put the first character back
|
|
while ((c = fgetc(file)) != EOF && !isspace(c) && c != ';') {
|
|
if (i < (int)(sizeof(buffer) - 1)) {
|
|
buffer[i++] = c;
|
|
} else {
|
|
fprintf(stderr, "Error: Token too long.\n");
|
|
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
|
|
exit(1);
|
|
}
|
|
}
|
|
buffer[i] = '\0'; // Null-terminate the token
|
|
|
|
// Put back the last character if it's not whitespace or EOF
|
|
if (c != EOF && c != ';') {
|
|
ungetc(c, file);
|
|
} else if (c == ';') {
|
|
// Skip remaining characters on this line if a comment starts
|
|
while ((c = fgetc(file)) != EOF && c != '\n');
|
|
}
|
|
|
|
// Store the token in intermediateElement and set a default type
|
|
currentElement->token = strdup(buffer);
|
|
currentElement->type = UNKNOWN;
|
|
|
|
return 1; // Success
|
|
}
|