readToken consumed a string literal character by character and counted none of the newlines in it, so every line number after a multi-line string was short by one - and by one more for each one after that. cosmos.asm has seven such strings spanning thirteen lines, so by the end of the file the assembler was reporting labels thirteen lines early. That was never only a cosmetic problem: the same counter is what every error message names, so an error anywhere after the help text was pointing at somebody else's code, thirteen lines away, with no sign that it was doing so. Found because the symbol table started publishing line numbers, which made a wrong one something you could look at. Verified against all 797 of cosmos.asm's labels, every one of which now names the line it is actually on. The check that keeps it that way comes with the next commit, which is what gives it something to check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
374 lines
16 KiB
C
374 lines
16 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, '.');
|
|
}
|
|
|
|
int found = getOpcode(token);
|
|
if (found == NOT_AN_OPCODE) {
|
|
return 0;
|
|
}
|
|
uint8_t opcode = (uint8_t)found;
|
|
|
|
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 != '"') {
|
|
// A STRING MAY HAVE NEWLINES IN IT, and they are as real as any others. Not
|
|
// counting them is why every line number after one was short: cosmos.asm has
|
|
// thirteen of them and its last label was reported thirteen lines early, which
|
|
// makes an error message point at somebody else's code. Counted here, where
|
|
// the character is consumed, rather than by scanning the token afterwards -
|
|
// the same newline must not be counted twice if this loop ever grows a way out.
|
|
if (c == '\n') {
|
|
(*lineNumber)++;
|
|
}
|
|
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
|
|
}
|