A symbol table says which memory, and where the name was written

The dump was an address and a name. Both of the questions it gets asked
were only half answered.

"What is at this address" was ambiguous, because Program and Data are
separate memories and an address alone does not say which one. That is
easy to miss in a loadable program, where the segments are usually based
far apart - and immediate in a boot image, where both start at zero:
replCalculator has a Program 0003 and a Data 0003 and the old file
printed both as "0003 <name>".

"Where is this defined" was not answered at all, and it is the one that
matters more as a program grows. A name defined once and called in forty
places is hard to find by searching. Lander's table names five files
besides its own; CosmOS and its libraries define over a thousand names
across a dozen.

So: memory, address, name, file, line, separated by tabs, sorted by
memory and then address with Program first. Tabs because that makes it a
table cut, awk and sort already read, and no heading line because
nothing should have to know to skip one. Everything needed was already
being passed to addLabel and thrown away; the file name points at the
copy the include list owns, which outlives the label table.

The manual describes the five fields, and docs.sh now settles that
description against a real dump - the shape, not the values, so that an
example cannot go stale and turn editing a program into editing a
manual. Verified with break.sh three ways: a reordered field, a dropped
field, and a field renamed in the manual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-05 10:21:06 -04:00
co-authored by Claude Opus 5
parent 7a55cfe151
commit 3527812c41
4 changed files with 139 additions and 14 deletions
+42 -12
View File
@@ -23,19 +23,38 @@ int labelCount = 0;
// ---- Where everything ended up ----
//
// Every label and the address it was given, in address order. The assembler knows this and
// nothing else does: a program on the disk is bytes, and the machine's own monitor can
// disassemble it but has no idea what any of it is called.
// Every label with the memory and address it was given and the place it was written, one
// per line, fields separated by tabs. The assembler knows all of this and nothing else
// does: a program on the disk is bytes, and the machine's own monitor can disassemble it
// but has no idea what any of it is called or where it came from.
//
// WHAT IT IS FOR is telling where a program spends its time. Counting which addresses get
// called says a great deal and names nothing, so the answer arrives as a list of numbers
// and somebody has to work out by hand which routine each one is inside. With this, a
// tally of call targets becomes a list of routine names.
// WHAT IT IS FOR is two questions. The first is where a program spends its time - counting
// which addresses get called says a great deal and names nothing, so the answer arrives as
// a list of numbers and somebody has to work out by hand which routine each one is inside.
// The second is where a name lives, which matters more the bigger the program gets: CosmOS
// and its libraries define over a thousand names across a dozen files, and finding the one
// definition of a routine means grepping for it and reading past every place it is called.
// The file and line answer that outright.
//
// Sorted by address rather than by name, because the question asked of it is always "what
// is at this address", and a label table is small enough that sorting it is free.
static int byAddress(const void *left, const void *right) {
// TABS, so that the file is a table every ordinary tool already understands - cut -f3, awk
// -F'\t', sort -k1,2 - and so that a name never has to be quoted. No header line, for the
// same reason: nothing that reads it should have to know to skip one.
//
// THE MEMORY IS THE FIRST FIELD BECAUSE THIS IS A HARVARD MACHINE. Program and Data are
// separate address spaces, so 0x3000 is two different places and an address alone does not
// say which. That is invisible in a loadable program, where the two segments are based
// somewhere apart, and immediate in a boot image, where both start at zero and every
// address in the file appears twice.
//
// Sorted by memory and then address rather than by name, because the question asked of it
// is always "what is at this address"; sorting by address alone would interleave two
// address spaces and make the first column flicker between them. A label table is small
// enough that sorting it is free.
static int byPlace(const void *left, const void *right) {
const Label *a = left, *b = right;
if (a->type != b->type) {
return a->type < b->type ? -1 : 1; // PROGRAM is 1 and DATA is 2, so code first.
}
if (a->address != b->address) {
return a->address < b->address ? -1 : 1;
}
@@ -55,9 +74,18 @@ void writeSymbolFile(const char *path) {
exit(1);
}
memcpy(sorted, labelArray, (size_t)labelCount * sizeof(Label));
qsort(sorted, (size_t)labelCount, sizeof(Label), byAddress);
qsort(sorted, (size_t)labelCount, sizeof(Label), byPlace);
for (int i = 0; i < labelCount; i++) {
fprintf(file, "%04X %s\n", sorted[i].address, sorted[i].label);
// A label is put in one of the two segments by populateLabelTable and there is no
// third, but the name is printed from the type rather than assumed, so a label that
// somehow arrived as neither says so instead of being filed under Data.
const char *memory = sorted[i].type == PROGRAM ? "Program"
: sorted[i].type == DATA ? "Data"
: "?";
fprintf(file, "%s\t%04X\t%s\t%s\t%d\n",
memory, sorted[i].address, sorted[i].label,
sorted[i].fileName ? sorted[i].fileName : "?",
sorted[i].lineNumber);
}
free(sorted);
fclose(file);
@@ -124,6 +152,8 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
labelArray[labelCount].label = cleanedLabel;
labelArray[labelCount].address = address;
labelArray[labelCount].type = type;
labelArray[labelCount].fileName = fileName;
labelArray[labelCount].lineNumber = lineNumber;
if (debugSecondPass) printf("Added label %s with address %04X\n", labelName, labelArray[labelCount].address);
labelCount++;
} else {
+9
View File
@@ -27,6 +27,15 @@ typedef struct {
char* label;
uint16_t address;
int type;
// Where the name was written. Only the symbol file reads these, and it is the source
// of the whole feature: an address tells you a routine exists and a file and line tell
// you where to go and read it.
//
// NEITHER IS OWNED HERE. fileName points at the copy the include list keeps, the same
// storage every intermediateElement points at, and the include list outlives the label
// table - assemblerCleanup frees the labels first. So freeLabelList must not free it.
const char* fileName;
int lineNumber;
} Label;
// Every label and the address it was given, in address order, so that a tally of