Files
SplitBit-Emulator/Source/Assembler/Assm-util.c
T
Anachronaut c3188ed657 Seventy becomes seventy one: a machine that can wait
HALT is terminal - stepCPU returns at once when the Halt Flag is up, so a
halted machine does not execute, service devices, or take an interrupt -
and that has to stay true, because every test ends with a halt and "halted"
is how a program says it has finished. The consequence was that SplitBit
had no way to wait at all. Every wait was a spin, and a spin is bus
traffic: 11.5% of Type over a 14K file on a disk of ten thousand cycles,
after read-ahead had already hidden three quarters of the latency.

WAIT is 0xFE, one byte, no operands, sitting under HALT where the
instruction that almost stops the machine belongs. Three decisions in it:

- A line already standing means there is nothing to wait for, so WAIT does
  nothing. That is what makes test-then-wait race-free.
- Any line ends the wait, masked or not, so a program can sleep on a device
  it has no handler for and read its status afterwards. Masking says who
  answers a request, not whether it happened.
- A line that wakes the CPU without being dispatched is taken down by the
  WAIT. Left standing it would be found by the next WAIT, which would
  return at once - the program would spin exactly as before while looking
  as though it slept.

Waiting is NOT a Status bit, and that is the trap avoided rather than a
gap: Status rides into the interrupt frame and comes back out, so a machine
interrupted mid-wait would return from its handler still waiting, and wait
again for what it had already been given. An internal field instead.

Idle cycles are counted apart from bus cycles and the halt line says so
when there are any, which is what makes the difference observable at all -
with the line-clearing removed the total moves by ONE cycle, 20,100 against
20,099, and only the idle half changes, halving to 9,976. A test on
totals could never have seen it. Tests/terminal.sh asks that question,
being the file for things a recorded output cannot see, and fails with the
clear removed while "both reads finished" still passes.

Three collisions, all found by building it:

- 0xFE was the assembler's "not an instruction" sentinel. getOpcode now
  answers a negative NOT_AN_OPCODE, which is outside the range of every
  possible answer instead of inside the unused part of it.
- 0xFE was also what faultTest and faultResumeTest executed to provoke a
  fault. They now use 0xFD and say why, because they did not fail when it
  became an instruction - they HUNG, having started sleeping instead.
- Keys.asm has had a label called "wait" for a year, and mnemonics are
  matched uppercased. What that reported was "Branch without label" at the
  BRQ thirty lines away. The assembler now refuses a label that is already
  an instruction, at the label, by name; every instruction added takes a
  word out of the space of label names, so this will happen again.
2026-08-26 11:11:25 -04:00

365 lines
15 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 != '"') {
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
}