Vectors in the symbol table, with both of the numbers they have

A vector is the one thing about a program that nothing else can tell
you. A pinned vector has its number in the source that pinned it, but a
vector the assembler numbered has that number nowhere at all - not in
the source, not in the binary in any form a reader can find. Until now
there was no way to learn that a vector became number 64.

It also cost two hops to follow by hand. The name in "SWI osPrintString"
is not the name of the routine that implements it, so finding the code
meant searching for the vector, reading the handler's name off the
Vector Segment, and searching again. A vector row now names the handler
and gives the line the two were tied together on.

Both numbers, at the user's asking, because neither can be worked out
from the other without knowing which table the vector is in: the Number
is what a program writes and the machine dispatches on, the Address is
where the handler's address is stored, base plus twice the number. The
slot is computed with the same expression the loader is given, so what
the table says and what gets written there cannot drift apart. A vector
a program only declares is listed too - that is how a program says which
vectors it calls, and how two programs can be checked against each other
for agreeing about a number.

A device has no name of its own, being named by the port it is plugged
into, so it is listed under its handler.

The first field is now Kind rather than Memory, because Vector and
Device are not memories. Sorted Program, Data, Vector, Device.

docs.sh checks the six fields against the manual and against real dumps
of two programs - Keys, a loadable program with all four kinds, and
cosmos, a boot image whose segments both start at zero. It now also
checks that a row's name really appears on the line the row names, which
is what catches the string-newline bug fixed in ca6c8ca coming back.
Verified with break.sh four ways: wrong slot arithmetic, vectors
dropped, a field renamed in the manual, and that bug reintroduced.

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:51:50 -04:00
co-authored by Claude Opus 5
parent ca6c8ca6ad
commit f3d8985bc4
4 changed files with 240 additions and 65 deletions
+92 -22
View File
@@ -837,37 +837,50 @@ if "## Running the Assembler:" not in am:
pass # Already reported above.
else:
section = am.split("## Running the Assembler:")[1].split("\n## ")[0]
said = re.findall(r'^\| (Memory|Address|Name|File|Line) \|', section, re.M)
if said != ["Memory", "Address", "Name", "File", "Line"]:
problems.append("the Assembler Manual does not describe the symbol file's five"
said = [row.split(" |")[0] for row in re.findall(r'^\| (\w+ \|.*)$', section, re.M)
if row.split(" |")[0] in ("Kind", "Address", "Name", "File", "Line", "Number")]
if said != ["Kind", "Address", "Name", "File", "Line", "Number"]:
problems.append("the Assembler Manual does not describe the symbol file's six"
" fields in order; it lists %s" % (said or "none"))
else:
with tempfile.TemporaryDirectory() as scratch:
# Two programs, because no one of them covers everything the format claims.
#
# Keys is a small LOADABLE program, where the two segments are based apart, and
# the smallest thing that produces all four kinds at once: labels in both
# memories, vectors it implements, vectors it only declares, and a device.
#
# cosmos is a BOOT IMAGE, where both segments start at zero, which is the case
# the kind field exists for - and it is the one that holds strings with newlines
# inside them, which is what the line numbers below go wrong on.
#
# Both assemble in a few thousandths of a second, so there is nothing to save by
# checking only one.
for program in ["Programs/CosmOS/Apps/Keys.asm",
"Programs/CosmOS/Source/cosmos.asm"]:
with tempfile.TemporaryDirectory() as scratch:
dump = os.path.join(scratch, "symbols")
# A program with labels in BOTH memories, so that both names the first field can
# take are seen. replCalculator is a boot image, which is also the case where the
# memory field is load bearing: both segments start at zero.
built = subprocess.run(["./Assembler", "-I", "Programs/Libraries",
"-I", "Programs/CosmOS/Source", "-S", dump,
"-o", os.path.join(scratch, "out.bin"),
"Programs/Examples/replCalculator.asm"],
"-o", os.path.join(scratch, "out"),
program],
capture_output=True, text=True)
if built.returncode != 0:
problems.append("could not assemble a program to check the symbol file")
problems.append("could not assemble %s to check the symbol file" % program)
else:
memories = set()
kinds = set()
for number, line in enumerate(open(dump).read().splitlines(), 1):
fields = line.split("\t")
if len(fields) != 5:
if len(fields) != 6:
problems.append("line %d of a symbol file has %d fields and the"
" Assembler Manual describes five"
" Assembler Manual describes six"
% (number, len(fields)))
break
memory, address, name, source, where = fields
memories.add(memory)
if memory not in ("Program", "Data"):
problems.append("a symbol file says a label is in %r, and the"
" Assembler Manual allows Program and Data" % memory)
kind, address, name, source, where, index = fields
kinds.add(kind)
if kind not in ("Program", "Data", "Vector", "Device"):
problems.append("a symbol file calls something a %r, and the"
" Assembler Manual allows Program, Data, Vector"
" and Device" % kind)
break
if not re.fullmatch(r'[0-9A-F]{4}', address):
problems.append("a symbol file gives %r as an address, and the"
@@ -878,13 +891,70 @@ else:
problems.append("a symbol file line is missing a name, a file or a"
" line number: %r" % line)
break
# A label has no number and says so with a dash; a vector has one, and
# the address it sits at has to be the slot that number works out to,
# which is the claim the manual makes about both fields at once.
if kind in ("Program", "Data"):
if index != "-":
problems.append("a %s label was given the number %r, and the"
" Assembler Manual says a label has none"
% (kind, index))
break
elif not index.isdigit():
problems.append("a %s row has %r where its number should be"
% (kind, index))
break
else:
base = 0xFC00 if kind == "Vector" else 0xFE00
if int(address, 16) != base + int(index) * 2:
problems.append("%s is number %s and the Assembler Manual puts"
" that at 0x%04X, but the symbol file says %s"
% (name, index, base + int(index) * 2, address))
break
else:
# Only meaningful if every line was well formed, which is what the else
# on the loop says.
if memories != {"Program", "Data"}:
problems.append("a program with labels in both memories produced a"
" symbol file naming only %s"
% ", ".join(sorted(memories)))
for wanted in ("Program", "Data", "Vector", "Device"):
if wanted not in kinds:
problems.append("a program with labels in both memories and a"
" Vector Segment produced a symbol file with no"
" %s row" % wanted)
# ---- And the line really is the line ----
#
# "Which line of that file, counting from one" is a claim, and it was
# wrong for years without anything noticing: a string literal with a
# newline inside it was read as one token and the newline was never
# counted, so every line number after one was short by one. cosmos.asm
# has thirteen, and its last label was reported thirteen lines early -
# which had been sending every error message after that point at
# somebody else's code, not only the symbol file.
#
# A name has to actually appear on the line its row names. That is a
# weak test of a right answer and a very strong test of a wrong one:
# drift lands on a line that has nothing to do with the name.
sources = {}
for row in open(dump).read().splitlines():
kind, address, name, source, where, index = row.split("\t")
if source not in sources:
try:
sources[source] = open(source).read().splitlines()
except OSError:
sources[source] = None
lines = sources[source]
if lines is None:
problems.append("a symbol file names %r, which cannot be read"
% source)
break
at = int(where)
if at < 1 or at > len(lines):
problems.append("%s is said to be on line %d of %s, which has"
" %d lines" % (name, at, source, len(lines)))
break
if not re.search(r'\b%s\b' % re.escape(name), lines[at - 1]):
problems.append("%s is said to be on line %d of %s, and that"
" line is %r"
% (name, at, source, lines[at - 1].strip()))
break
if problems:
print("The manuals and the code disagree:")