Say that an option's file is one the assembler writes, and check we said it

-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
This commit is contained in:
Anachronaut
2026-09-05 09:40:21 -04:00
co-authored by Claude Opus 5
parent cdee9acfae
commit 7a55cfe151
3 changed files with 65 additions and 5 deletions
+25 -4
View File
@@ -95,15 +95,28 @@ void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, cha
free(outputFileName); 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) { void printUsage(const char *programName) {
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName); printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
printf("\n"); 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("Options:\n");
printf(" -o <file> Write the output to this path instead of alongside the source.\n"); printf(" -o, --output <file> Write the assembled output here, instead of alongside the source.\n");
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n"); printf(" -I, --include <dir> Search 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(" -M, --depend <file> Write a make rule here, naming every source that went into the output.\n");
printf(" -S <file> Write every label and the address it was given, in address order.\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(" -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 // Writes a make rule naming every source file that went into the output, so that a
@@ -207,6 +220,14 @@ int main(int argc, char *argv[]) {
if (optind >= argc) { if (optind >= argc) {
fprintf(stderr, RED "Error: No source file specified.\n" RESET); 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]); printUsage(argv[0]);
return 1; return 1;
} }
+5
View File
@@ -420,8 +420,13 @@ Assembler [options] <sourcefile>
| -o, --output \<file\> | Write the output to this path. Without it, the output is named after the source file, in the directory the assembler was run from, taking .bin if it is a boot image and .sbx if it is a loadable program. | | -o, --output \<file\> | Write the output to this path. Without it, the output is named after the source file, in the directory the assembler was run from, taking .bin if it is a boot image and .sbx if it is a loadable program. |
| -I, --include \<dir\> | Look in this directory for included files. May be given more than once, and the directories are searched in the order given. | | -I, --include \<dir\> | Look in this directory for included files. May be given more than once, and the directories are searched in the order given. |
| -M, --depend \<file\> | Write out which source files went into the output, as a make rule. | | -M, --depend \<file\> | Write out which source files went into the output, as a make rule. |
| -S, --symbols \<file\> | Write every label and the address it was given, in address order, to this path. |
| -h, --help | Print the options and stop. | | -h, --help | Print the options and stop. |
Every option above that takes a \<file\> names a file the assembler WRITES, and the source is the bare argument at the end. So `Assembler -S program.asm` does not dump the symbols of program.asm. It asks for the symbol file to be called program.asm, and then finds it has no source left to assemble.
-S is for looking at what the assembler decided. Every label in the program, in address order, with the address it ended up at: which segment a name landed in, how far apart two routines really are, and whether the label you are looking for was assembled at all.
The assembler stops at the first error, says which file and line it was in, and exits without writing anything. The assembler stops at the first error, says which file and line it was in, and exits without writing anything.
## Building With Make: ## Building With Make:
+34
View File
@@ -172,6 +172,40 @@ else:
problems.append("%s (0x%02X) is a device class and has no row in the Devices" problems.append("%s (0x%02X) is a device class and has no row in the Devices"
" table" % (name, value)) " table" % (name, value))
# ---- Every option the assembler takes is written down in both places ----
#
# An option added to getopt is an option nobody knows about until it is said twice: in the
# manual's table, and in the usage the assembler prints when it is asked for help or refuses
# a command line. -S arrived with neither, and the damage was worse than an undocumented
# switch. The usage listed a bare "-S <file>" with no long name and no statement that the
# file is one it WRITES, so it read as "dump the symbols of <file>" - which hands the source
# to -S, leaves nothing positional behind it, and gets answered with "No source file
# specified" on a command line that plainly names one.
#
# The option table is the source of truth because it is what getopt_long is actually given.
assembler = read("Source/Assembler/Assembler.c")
options = re.findall(r'^\s*\{"([a-z]+)",\s*\w+,\s*0,\s*\'(\w)\'\s*\},', assembler, re.M)
if not options:
problems.append("could not find the assembler's option table")
elif "void printUsage" not in assembler:
problems.append("the assembler has lost its printUsage")
elif "## Running the Assembler:" not in am:
problems.append("the Assembler Manual has lost its options table")
else:
usage = assembler.split("void printUsage")[1].split("\n}")[0]
section = am.split("## Running the Assembler:")[1].split("\n## ")[0]
# Only the table rows count, so a switch merely mentioned in the prose below it does not
# pass for a documented one.
rows = re.findall(r'^\| (-\w, --[a-z]+)', section, re.M)
for longName, shortName in options:
spelled = "-%s, --%s" % (shortName, longName)
if spelled not in rows:
problems.append("the assembler takes %s and the Assembler Manual's option table"
" has no row for it" % spelled)
if spelled not in usage:
problems.append("the assembler takes %s and its own usage message does not name"
" it" % spelled)
# ---- The vector ranges the manuals quote are the ones the assembler uses ---- # ---- The vector ranges the manuals quote are the ones the assembler uses ----
# #
# Both manuals print the boundary between numbers a program may pin and numbers the # Both manuals print the boundary between numbers a program may pin and numbers the