365 lines
18 KiB
C
365 lines
18 KiB
C
// firstPass.c
|
|
// Functions for the 'first pass' of the SplitBit Assembler.
|
|
// The first pass' primary goal is to get all the assembly files loaded into the Intermediate Array.
|
|
// Because I want included files to load and assemble in-line with the current file, that needs to be handled at this stage.
|
|
// Written by Anachronaut
|
|
// 10/25/2024
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <libgen.h>
|
|
#include <limits.h>
|
|
#include "firstPass.h"
|
|
#include "Assm-util.h"
|
|
#include "assembly.h"
|
|
|
|
// 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++) {
|
|
free(includeList[i].path);
|
|
free(includeList[i].canonical);
|
|
}
|
|
includeCount = 0;
|
|
for (int i = 0; i < includeDirectoryCount; i++) {
|
|
free(includeDirectories[i]);
|
|
}
|
|
includeDirectoryCount = 0;
|
|
}
|
|
|
|
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].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;
|
|
}
|
|
|
|
// 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){
|
|
// 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;
|
|
// A base has to come before anything else in its segment, so this remembers whether
|
|
// that segment has had anything put in it yet.
|
|
static int segmentUsed[3] = {0, 0, 0};
|
|
int lineNumber = 1; // Line numbers start at 1.
|
|
// Open the file.
|
|
FILE *file = fopen(fileName, "r");
|
|
if (!file) {
|
|
fprintf(stderr, RED "Error: Couldn't open file \"%s\"\n" RESET, fileName);
|
|
exit(1);
|
|
}
|
|
// Read off tokens.
|
|
while (readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
|
if ((size_t)*intermediateIndex >= *arraySize - 1) {
|
|
size_t grownSize = *arraySize * 2; // Double the size of the array.
|
|
// Into a temporary, so that the old allocation is still ours to free if
|
|
// this fails, rather than being lost the moment realloc returns NULL.
|
|
intermediateElement *grown = realloc(*intermediateArray, grownSize * sizeof(intermediateElement));
|
|
if (!grown) {
|
|
fprintf(stderr, RED "Error: Memory reallocation failed.\n" RESET);
|
|
exit(1);
|
|
}
|
|
// New elements have to start blank. realloc leaves the new space holding
|
|
// whatever the heap had in it before, and an element that never sets its
|
|
// own byteLength, such as a keyword or a label definition, would then add
|
|
// rubbish to the running address and move everything after it.
|
|
memset(grown + *arraySize, 0, (grownSize - *arraySize) * sizeof(intermediateElement));
|
|
*intermediateArray = grown;
|
|
*arraySize = grownSize;
|
|
}
|
|
//printf("Token number %d\n", intermediateIndex);
|
|
// Go ahead and mark what we already know about this token.
|
|
(*intermediateArray)[*intermediateIndex].fileName = fileName;
|
|
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
|
|
// Take a look at it and determine what it is.
|
|
int testValue = checkIfKeyword(&(*intermediateArray)[*intermediateIndex]);
|
|
if(testValue > 0) {
|
|
switch (testValue){
|
|
case KEYWORD_INCLUDE: {
|
|
// We need to load another file and process it before continuing.
|
|
status = NOWHERE;
|
|
// Get the filename and work out where it actually is.
|
|
(*intermediateIndex)++;
|
|
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
|
// The file ended straight after the keyword, so there is no
|
|
// name to read and nothing sensible to go looking for.
|
|
fprintf(stderr, RED "Error: #Include without a file name.\n" RESET);
|
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
char *requested = (*intermediateArray)[*intermediateIndex].token;
|
|
char *resolved = resolveInclude(fileName, requested);
|
|
if (!resolved) {
|
|
reportMissingInclude(fileName, requested, lineNumber);
|
|
exit(1);
|
|
}
|
|
// The included file's first token is about to be read into this
|
|
// same slot, so let the file name go now. Leaving it would strand
|
|
// the only pointer to it the moment it is overwritten.
|
|
free((*intermediateArray)[*intermediateIndex].token);
|
|
(*intermediateArray)[*intermediateIndex].token = NULL;
|
|
// 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.
|
|
status = PROGRAM;
|
|
break;
|
|
case KEYWORD_DATA:
|
|
// Set the state to DATA so we mark additional tokens for inclusion into Data Memory.
|
|
status = DATA;
|
|
break;
|
|
case KEYWORD_BASE: {
|
|
if (status != PROGRAM && status != DATA) {
|
|
fprintf(stderr, RED "Error: #Base outside the Program or Data Segment.\n There is no segment for it to be the base of.\n" RESET);
|
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
if (segmentUsed[status]) {
|
|
fprintf(stderr, RED "Error: #Base after something is already in the segment.\n"
|
|
" A base says where the whole segment begins, so it has to come first.\n" RESET);
|
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
(*intermediateIndex)++;
|
|
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
|
fprintf(stderr, RED "Error: #Base without an address.\n" RESET);
|
|
exit(1);
|
|
}
|
|
(*intermediateArray)[*intermediateIndex].fileName = fileName;
|
|
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
|
|
setSegmentBase(status, readAddress(&(*intermediateArray)[*intermediateIndex], "#Base"));
|
|
(*intermediateArray)[*intermediateIndex].type = KEYWORD;
|
|
(*intermediateArray)[*intermediateIndex].byteLength = 0;
|
|
(*intermediateArray)[*intermediateIndex].destination = NOWHERE;
|
|
(*intermediateArray)[*intermediateIndex - 1].destination = NOWHERE;
|
|
(*intermediateArray)[*intermediateIndex - 1].byteLength = 0;
|
|
(*intermediateIndex)++;
|
|
continue;
|
|
}
|
|
case KEYWORD_ALIGN:
|
|
case KEYWORD_RESERVE: {
|
|
// Both take a count, and both only make sense somewhere that has a
|
|
// cursor to move along.
|
|
const char *what = (testValue == KEYWORD_ALIGN) ? "#Align" : "#Reserve";
|
|
if (status != PROGRAM && status != DATA) {
|
|
fprintf(stderr, RED "Error: %s outside the Program or Data Segment.\n There is nothing there for it to move along.\n" RESET, what);
|
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
// The count is read here rather than being left to the literal check,
|
|
// because it is an instruction to the assembler and never becomes a
|
|
// byte, so a byte's range would be the wrong limit for it. A page
|
|
// alignment needs 256, and a reservation is often far larger.
|
|
intermediateElement *directive = &(*intermediateArray)[*intermediateIndex];
|
|
(*intermediateIndex)++;
|
|
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
|
|
fprintf(stderr, RED "Error: %s without a number.\n" RESET, what);
|
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
(*intermediateArray)[*intermediateIndex].fileName = fileName;
|
|
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
|
|
uint16_t count = readCount(&(*intermediateArray)[*intermediateIndex], what);
|
|
|
|
// The count token itself contributes nothing; the directive carries
|
|
// everything, so that one element stands for one run of zeroes.
|
|
(*intermediateArray)[*intermediateIndex].type = KEYWORD;
|
|
(*intermediateArray)[*intermediateIndex].byteLength = 0;
|
|
(*intermediateArray)[*intermediateIndex].destination = NOWHERE;
|
|
|
|
directive->destination = status;
|
|
if (testValue == KEYWORD_ALIGN) {
|
|
// How many zeroes this comes to depends on where the cursor has
|
|
// reached, which is not known until the second pass walks it.
|
|
directive->type = ALIGNMENT;
|
|
directive->address = count;
|
|
directive->byteLength = 0;
|
|
} else {
|
|
directive->type = PADDING;
|
|
directive->byteLength = count;
|
|
}
|
|
(*intermediateIndex)++;
|
|
continue;
|
|
}
|
|
case KEYWORD_VECTORS:
|
|
// Set the state to VECTORS. Tokens from here on name handlers rather
|
|
// than becoming bytes, and the second pass reads them.
|
|
status = VECTORS;
|
|
break;
|
|
}
|
|
// Next, check to see if it's an instruction.
|
|
} else if (checkIfInstruction(&(*intermediateArray)[*intermediateIndex])) {
|
|
// We should check if we're set up to mark this for the Program Segment.
|
|
if (status != PROGRAM) {
|
|
fprintf(stderr, RED "Error: Attempting to assemble outside the Program Segment.\n Did you forget to use the #Program keyword?\n" RESET);
|
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
// Next, check if it's a literal value. A string is never one, however it begins:
|
|
// the quotes are gone by now, so a string starting with a zero looks exactly like
|
|
// a malformed literal and used to be rejected as one.
|
|
} else if ((*intermediateArray)[*intermediateIndex].type != STRING
|
|
&& checkIfLiteralValue(&(*intermediateArray)[*intermediateIndex])) {
|
|
// We should check to make sure we have a destination for it.
|
|
if (status == NOWHERE) {
|
|
fprintf(stderr, RED "Error: Attempting to write a value to nowhere!\n Did you forget to use the #Program or #Data keyword?\n" RESET);
|
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
// Finally, check if it's a label or label definition.
|
|
// First, make sure it hasn't already been marked as a string literal.
|
|
} else if ((*intermediateArray)[*intermediateIndex].type != STRING) {
|
|
if (checkIfLabel(&(*intermediateArray)[*intermediateIndex])) {
|
|
if (status == NOWHERE) {
|
|
fprintf(stderr, RED "Error: Attempting to create or use a label nowhere!\n Did you forget to use the #Program or #Data keyword?\n" RESET);
|
|
printf(" File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
// A name written after SWI is a vector rather than an address, so it
|
|
// stands for one byte instead of two. This is settled by what the name
|
|
// follows, so that it does not depend on the Vector Segment having been
|
|
// read first, which it may not have been: it can live in another file.
|
|
if (status == PROGRAM
|
|
&& (*intermediateArray)[*intermediateIndex].type == LABEL
|
|
&& *intermediateIndex > 0
|
|
&& (*intermediateArray)[*intermediateIndex - 1].type == INSTRUCTION
|
|
&& (*intermediateArray)[*intermediateIndex - 1].byteValue == 0x18) {
|
|
(*intermediateArray)[*intermediateIndex].type = VECTOR_REFERENCE;
|
|
(*intermediateArray)[*intermediateIndex].byteLength = 1;
|
|
}
|
|
}
|
|
}
|
|
(*intermediateArray)[*intermediateIndex].destination = status;
|
|
if ((status == PROGRAM || status == DATA)
|
|
&& (*intermediateArray)[*intermediateIndex].type != KEYWORD) {
|
|
segmentUsed[status] = 1;
|
|
}
|
|
(*intermediateIndex)++;
|
|
}
|
|
return 0;
|
|
}
|