It always wrote .bin, whatever it had built. So assembling a loadable
program without -o produced Say.bin containing SBEX - a boot image name on a
file the machine cannot boot, in a repository whose whole convention is that
a .bin is started from and a .sbx is loaded.
Successfully wrote SplitBit boot image to "hello.bin".
Successfully wrote SplitBit loadable program to "Say.sbx".
programIsLoadable() already existed and is already what decides which
writer runs; the name now asks it too. Nothing in the build depended on the
old behaviour, because everything that assembles anything passes -o.
THE ASSEMBLER THAT RUNS ON SPLITBIT ALREADY DID IT THIS WAY. Two assemblers
naming their output differently from the same source is exactly the kind of
difference that wastes an afternoon, and the newer one was right.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
256 lines
9.8 KiB
C
256 lines
9.8 KiB
C
// Assembler.c
|
|
// Basic Assembler for programs written for the SplitBit CPU
|
|
// Written by Anachronaut
|
|
// 10/18/2024
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <libgen.h>
|
|
#include <unistd.h>
|
|
#include <getopt.h>
|
|
#include "Assm-util.h"
|
|
#include "firstPass.h"
|
|
#include "secondPass.h"
|
|
|
|
int programLength = 0;
|
|
int dataLength = 0;
|
|
|
|
uint8_t Program[0xFFFF], Data[0xFFFF];
|
|
|
|
|
|
// What an assembled file is called when nobody said. The extension follows the FORMAT
|
|
// rather than being always .bin: a boot image is what the machine starts from and a
|
|
// loadable program is what a running system loads, and this repository has called them
|
|
// .bin and .sbx apart for long enough that a .bin holding SBEX is a small lie.
|
|
//
|
|
// The assembler that runs on SplitBit already chose this way. Two assemblers naming their
|
|
// output differently from the same source is exactly the sort of difference that wastes an
|
|
// afternoon.
|
|
char* createOutputFileName(const char *inputFilePath) {
|
|
const char *extension = programIsLoadable() ? ".sbx" : ".bin";
|
|
// Make a copy of inputFilePath, since basename may modify it
|
|
char *pathCopy = strdup(inputFilePath);
|
|
if (!pathCopy) {
|
|
fprintf(stderr, "Error: Memory allocation failed for path copy.\n");
|
|
exit(1);
|
|
}
|
|
|
|
// Get the filename from the path
|
|
char *fileName = basename(pathCopy);
|
|
|
|
// Find the length of the filename
|
|
size_t len = strlen(fileName);
|
|
|
|
// Check if the filename ends with ".asm"
|
|
char *outputFileName;
|
|
if (len > 4 && strcmp(fileName + len - 4, ".asm") == 0) {
|
|
// Remove ".asm" (4 chars) and add the extension (4 chars plus a null terminator).
|
|
outputFileName = malloc(len - 4 + 5);
|
|
if (!outputFileName) {
|
|
fprintf(stderr, "Error: Memory allocation failed for output file name.\n");
|
|
free(pathCopy);
|
|
exit(1);
|
|
}
|
|
strncpy(outputFileName, fileName, len - 4);
|
|
strcpy(outputFileName + len - 4, extension);
|
|
} else {
|
|
// No ".asm" to replace, so the extension goes on the end of the whole name.
|
|
outputFileName = malloc(len + 5);
|
|
if (!outputFileName) {
|
|
fprintf(stderr, "Error: Memory allocation failed for output file name.\n");
|
|
free(pathCopy);
|
|
exit(1);
|
|
}
|
|
strcpy(outputFileName, fileName);
|
|
strcat(outputFileName, extension);
|
|
}
|
|
|
|
free(pathCopy); // Free the temporary path copy
|
|
return outputFileName;
|
|
}
|
|
|
|
// Remember to be a good programmer and free up all the allocated memory.
|
|
void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, char *outputFileName) {
|
|
// Free each token in intermediateArray.
|
|
for (int i = 0; i < arraySize; i++) {
|
|
if (intermediateArray[i].token) {
|
|
free(intermediateArray[i].token);
|
|
}
|
|
}
|
|
// Free the intermediateArray itself.
|
|
free(intermediateArray);
|
|
|
|
// Free the list of included files.
|
|
freeIncludeList();
|
|
|
|
// Free the list of labels.
|
|
freeLabelList();
|
|
|
|
// Free the list of vectors.
|
|
freeVectorList();
|
|
|
|
// Free the output file name
|
|
free(outputFileName);
|
|
}
|
|
|
|
void printUsage(const char *programName) {
|
|
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
|
|
printf("\n");
|
|
printf("Options:\n");
|
|
printf(" -o <file> Write the output to this path instead of alongside the source.\n");
|
|
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n");
|
|
printf(" -M <file> Write the source files this output depends on, as a make rule.\n");
|
|
printf(" -h, --help Display this help message.\n");
|
|
}
|
|
|
|
// Writes a make rule naming every source file that went into the output, so that a
|
|
// build system knows to reassemble when any of them changes. The empty rules after it
|
|
// are so that deleting a library does not leave make with a prerequisite it cannot
|
|
// build; without them the build stops instead of just reassembling.
|
|
void writeDependencyFile(const char *dependencyPath, const char *outputPath) {
|
|
FILE *file = fopen(dependencyPath, "w");
|
|
if (!file) {
|
|
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, dependencyPath);
|
|
exit(1);
|
|
}
|
|
fprintf(file, "%s:", outputPath);
|
|
for (int i = 0; i < sourceFileCount(); i++) {
|
|
fprintf(file, " %s", sourceFile(i));
|
|
}
|
|
fprintf(file, "\n\n");
|
|
// The source itself is index 0, and always exists, so it needs no empty rule.
|
|
for (int i = 1; i < sourceFileCount(); i++) {
|
|
fprintf(file, "%s:\n", sourceFile(i));
|
|
}
|
|
fclose(file);
|
|
}
|
|
|
|
// A program that bases one segment and not the other is a mistake the assembler is the
|
|
// last place to catch. Nothing relocates, so the unbased half keeps the addresses it was
|
|
// given, which are addresses from zero up, and the loader puts it there: on top of
|
|
// whatever the system keeps at the bottom of memory. It does not fail at load time and it
|
|
// does not fail at the jump. It fails later, somewhere else, as corruption.
|
|
//
|
|
// Only a segment with something in it can land on anything, so an empty one says nothing.
|
|
// A base of zero that was actually asked for is left alone, which is how a program says
|
|
// it meant it.
|
|
void checkSegmentBases(const char *fileName) {
|
|
if (!programIsLoadable()) {
|
|
return; // A boot image. Both segments begin at zero because that is where they go.
|
|
}
|
|
|
|
const char *segmentName[3];
|
|
segmentName[PROGRAM] = "Program";
|
|
segmentName[DATA] = "Data";
|
|
int segmentEnd[3];
|
|
segmentEnd[PROGRAM] = programLength;
|
|
segmentEnd[DATA] = dataLength;
|
|
|
|
const int segments[2] = { PROGRAM, DATA };
|
|
for (int i = 0; i < 2; i++) {
|
|
int mine = segments[i], other = segments[1 - i];
|
|
int content = segmentEnd[mine] - segmentBase(mine);
|
|
if (segmentBaseWasGiven(mine) || content <= 0 || !segmentBaseWasGiven(other)) {
|
|
continue;
|
|
}
|
|
fprintf(stderr, RED "Error: The %s Segment is based at 0x%04X, but the %s Segment\n"
|
|
" has %d byte%s at 0x0000 and was never given a #Base.\n"
|
|
" Half a program loaded at zero lands on whatever is already there.\n" RESET,
|
|
segmentName[other], segmentBase(other), segmentName[mine],
|
|
content, content == 1 ? "" : "s");
|
|
printf(" File: %s\n", fileName);
|
|
printf(" Say \"#Base 0x0000\" in the %s Segment if that is what you meant.\n", segmentName[mine]);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
static struct option long_options[] = {
|
|
{"output", required_argument, 0, 'o'},
|
|
{"include", required_argument, 0, 'I'},
|
|
{"depend", required_argument, 0, 'M'},
|
|
{"help", no_argument, 0, 'h'},
|
|
{0, 0, 0, 0 }
|
|
};
|
|
char *outputFileName = NULL;
|
|
char *dependencyFileName = NULL;
|
|
int option_index = 0;
|
|
int opt;
|
|
|
|
while ((opt = getopt_long(argc, argv, "o:I:M:h", long_options, &option_index)) != -1) {
|
|
switch (opt) {
|
|
case 'o':
|
|
outputFileName = strdup(optarg);
|
|
break;
|
|
case 'I':
|
|
addIncludeDirectory(optarg);
|
|
break;
|
|
case 'M':
|
|
dependencyFileName = strdup(optarg);
|
|
break;
|
|
case 'h':
|
|
printUsage(argv[0]);
|
|
return 0;
|
|
default:
|
|
printUsage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (optind >= argc) {
|
|
fprintf(stderr, RED "Error: No source file specified.\n" RESET);
|
|
printUsage(argv[0]);
|
|
return 1;
|
|
}
|
|
char *fileName = argv[optind];
|
|
optind++;
|
|
if (optind < argc) {
|
|
fprintf(stderr, RED "Error: Unexpected argument: %s\n" RESET, argv[optind]);
|
|
return 1;
|
|
}
|
|
|
|
// Allocate initial space for the intermediate array. calloc rather than malloc,
|
|
// because not every element sets every one of its own fields, and a stray
|
|
// byteLength would quietly shift every address that follows it.
|
|
size_t arraySize = 1024;
|
|
intermediateElement *intermediateArray = calloc(arraySize, sizeof(intermediateElement));
|
|
if (!intermediateArray) {
|
|
fprintf(stderr, RED "Error: Memory allocation failed.\n" RESET);
|
|
exit(1);
|
|
}
|
|
|
|
// The file named on the command line is the first source file, and the include
|
|
// list owns the copy that everything else points at.
|
|
char *source = recordSourceFile(strdup(fileName));
|
|
int index = 0;
|
|
loadFile(&intermediateArray, source, &index, &arraySize);
|
|
populateLabelTable(intermediateArray, index);
|
|
fillInLabelAddresses(intermediateArray, index);
|
|
// Vectors come after the labels, because a handler is named by its label, and before
|
|
// the buffers are filled, because SWI needs the number its vector was given.
|
|
populateVectorTable(intermediateArray, index);
|
|
fillInVectorReferences(intermediateArray, index);
|
|
// The buffers are filled from wherever each segment is based, so that a byte's place
|
|
// in the buffer is the address it will have. For a boot image both bases are zero and
|
|
// this changes nothing.
|
|
programLength = segmentBase(PROGRAM);
|
|
dataLength = segmentBase(DATA);
|
|
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
|
|
// After the buffers, because how much a segment actually holds is not known until it
|
|
// has been filled, and an empty segment is not a mistake.
|
|
checkSegmentBases(fileName);
|
|
if (!outputFileName) {
|
|
outputFileName = createOutputFileName(fileName);
|
|
}
|
|
writeOutputFile(outputFileName, Program, programLength, Data, dataLength);
|
|
if (dependencyFileName) {
|
|
writeDependencyFile(dependencyFileName, outputFileName);
|
|
free(dependencyFileName);
|
|
}
|
|
assemblerCleanup(intermediateArray, index, outputFileName);
|
|
return 0;
|
|
}
|
|
|