-S was added without a row in the Assembler Manual, and the usage it printed listed a bare "-S <file>" with no long name and no statement of what the file is for. That is not merely incomplete, it is misleading: "-S <file>" reads just as naturally as "dump the symbols of <file>", and asking for it that way hands the source to -S, leaves nothing positional behind it, and is answered with "No source file specified" on a command line that plainly names a source. The error described the hole the mistake left and hid the mistake. So the usage now prints the long names, says outright that every <file> is a path it writes and the source is the last argument on its own, and ends with a whole example command. When the source is missing and a file-taking option was given, the error says which options take a path to write. The manual gains the -S row it never had, a warning in the same words, and a sentence on what a symbol dump is for. Documenting it twice is how it went wrong once, so docs.sh now settles both against getopt's own option table: every option the assembler takes has a row in the manual and a line in its own usage. Verified with break.sh against the manual row and the usage line separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
288 lines
12 KiB
C
288 lines
12 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);
|
|
}
|
|
|
|
// EVERY OPTION HERE NAMES A FILE THE ASSEMBLER WRITES, and the source is the bare argument
|
|
// at the end. A list of bare flags does not say that: "-S <file>" reads just as naturally as
|
|
// "dump the symbols of <file>", and asking for it that way hands the source to -S, leaves
|
|
// nothing positional behind it, and gets answered with "No source file specified" - which
|
|
// describes the symptom and hides the cause. So the long names are printed, every line says
|
|
// where the file is written, and an example shows a whole command rather than leaving one to
|
|
// be assembled out of the parts.
|
|
void printUsage(const char *programName) {
|
|
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
|
|
printf("\n");
|
|
printf("Assembles one source file. The source is the last argument, on its own; every\n");
|
|
printf("<file> below is a path the assembler writes, and every <dir> one it searches.\n");
|
|
printf("\n");
|
|
printf("Options:\n");
|
|
printf(" -o, --output <file> Write the assembled output here, instead of alongside the source.\n");
|
|
printf(" -I, --include <dir> Search this directory for included files. May be given more than once.\n");
|
|
printf(" -M, --depend <file> Write a make rule here, naming every source that went into the output.\n");
|
|
printf(" -S, --symbols <file> Write every label here with the address it was given, in address order.\n");
|
|
printf(" -h, --help Display this help message.\n");
|
|
printf("\n");
|
|
printf("Example:\n");
|
|
printf(" %s -I Libraries -S game.sym -o game.sbx game.asm\n", programName);
|
|
}
|
|
|
|
// 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'},
|
|
{"symbols", required_argument, 0, 'S'},
|
|
{"help", no_argument, 0, 'h'},
|
|
{0, 0, 0, 0 }
|
|
};
|
|
char *outputFileName = NULL;
|
|
char *dependencyFileName = NULL;
|
|
char *symbolFileName = NULL;
|
|
int option_index = 0;
|
|
int opt;
|
|
|
|
while ((opt = getopt_long(argc, argv, "o:I:M:S: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 'S':
|
|
symbolFileName = 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);
|
|
// The commonest way to get here is giving the source to an option that wanted a
|
|
// path to write, which consumes it and leaves nothing positional. Saying so is the
|
|
// difference between an error that names the mistake and one that names the hole
|
|
// the mistake left behind.
|
|
if (outputFileName || dependencyFileName || symbolFileName) {
|
|
fprintf(stderr, " -o, -M and -S each name a file to WRITE, not one to read.\n"
|
|
" The source file is the last argument, on its own.\n");
|
|
}
|
|
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);
|
|
}
|
|
// Before the cleanup, which is what frees the label table this reads.
|
|
if (symbolFileName) {
|
|
writeSymbolFile(symbolFileName);
|
|
free(symbolFileName);
|
|
}
|
|
assemblerCleanup(intermediateArray, index, outputFileName);
|
|
return 0;
|
|
}
|
|
|