Files
Anachronaut cd5f548736 Move the opcode map: nothing in 0x0X, and room for a return variant
Three blocks move and nothing else changes. Branches take 0x60, subroutines
take 0x70, and the ALU moves up into the 0x10 block the two of them used to
share. Order within each block is preserved exactly - this relocates them,
it does not rethink them.

WHAT IT BUYS IS AN EMPTY 0x00 TO 0x0F. Program Memory that was never
written, or a load that stopped part way and left zeroes in its tail, used
to read as a long run of ADDs: the machine carried on through them, arrived
somewhere unpredictable, and whatever broke there was a long way from the
byte that caused it. Now it faults where it is met:

  Fault: 0x00 at Program Address 0x0004 is not an instruction.

That is the address of the byte after the last real instruction, which is
the difference between a diagnosis and a search. Reserving the whole nibble
rather than just 0x00 means a run into blank memory faults wherever it
starts rather than only when it lands on the right byte. runOffTest records
it, and the block is left empty for whatever turns out to want it.

The other half is room: branches and subroutines had filled 0x10 to 0x1F
between them, so a service return that keeps Q and DP3 had nowhere to sit
next to its family. It has 0x76 waiting now.

Five places wrote an opcode down that the scripted remap did not reach, and
four of them were found by tests rather than by looking:

- secondPass.c lists which opcodes take an address, and firstPass.c knows
  SWI by number. Missing those made XOR read as a branch.
- Asm.asm knows SWI by number too, being the other assembler. Missing it
  made the native and host assemblers disagree byte for byte, which is
  exactly the check that exists to catch a thing known in two places.
- loaderTest.asm carries a hand written payload, and its RETI was 0x19. To
  the assembler those are numbers and to the program they are data, so
  nothing but running it could notice. It says so in a comment now.
- The Assembler Manual prints the bytes hello.asm assembles to, and two of
  them were branches.

The monitor's recorded disassembly moved by exactly the bytes it should:
18 became 72 wherever SWI appears, with SETD and INIB untouched and every
disassembled line still reading the same.
2026-08-27 18:05:54 -04:00

403 lines
20 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.
//
// ROOM IS MADE BEFORE THE TOKEN IS READ, not after. readToken writes into the element
// at the current index, so a check that came afterwards was checking whether the write
// that had already happened was allowed to. It survived for a long time because the
// margin usually covered it, and stopped surviving when a file grew past a doubling:
// several paths below take a SECOND element for one token - an #Include takes one for
// the file name, #Align and #Reserve take one for the count - so the index can move by
// two in an iteration and step straight over a margin of one.
//
// The margin is two for that reason, which is the most any one iteration uses.
while (1) {
if ((size_t)*intermediateIndex + 2 >= *arraySize) {
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;
}
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
break;
}
// 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. A string is never one, however it is
// spelled: the quotes are gone by the time anything looks at a token, so a string
// whose text happens to be a mnemonic looked exactly like that instruction and was
// assembled as one. Mnemonics are matched without regard to case, so this was not
// only a problem for a program with "ADD" in its data - "or" and "and" are ordinary
// enough words to find in a message.
} else if ((*intermediateArray)[*intermediateIndex].type != STRING
&& 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);
}
// A string, which only the Data Segment can hold. Nothing below this line looks at
// strings, so without this they would fall past every check and out the bottom,
// and a string written anywhere else would assemble to nothing at all and say so
// to nobody.
} else if ((*intermediateArray)[*intermediateIndex].type == STRING) {
if (status == PROGRAM) {
fprintf(stderr, RED "Error: A string cannot go in the Program Segment.\n"
" Strings live in Data Memory, which is the only memory an instruction\n"
" can read. A string in Program Memory could not be reached even by the\n"
" program holding it, except through the memory controller.\n" RESET);
printf(" File: %s at line %d.\n", fileName, lineNumber);
printf(" The string: \"%s\"\n", (*intermediateArray)[*intermediateIndex].token);
printf(" Move it below a #Data line.\n");
exit(1);
}
if (status != DATA) {
fprintf(stderr, RED "Error: Attempting to write a string to nowhere!\n Did you forget to use the #Data keyword?\n" RESET);
printf(" File: %s at line %d.\n", fileName, lineNumber);
printf(" The string: \"%s\"\n", (*intermediateArray)[*intermediateIndex].token);
exit(1);
}
// Finally, check if it's a label or label definition.
} else {
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 == 0x72) {
(*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;
}