The pymdl book

38. Era artifacts

Everything in this book that is the era's rather than pymdl's came off a tape. This chapter is about the tapes: what the archive is, what pymdl took from it and where that lives, the formats those files are in and how they are read, and the discipline that keeps a copy honest about where it came from and whether it still matches.

38.1 The archive

MDL files, 1971-1987 is a collection published by MIT's Department of Distinctive Collections from the Tapes of Tech Square (ToTS, MC-0741): the MDL-specific files from 64 tape images, extracted with itstar (for ITS DUMP tapes) or the TOPS-20 equivalent, laid out as mdl/<tape>/<directory>/<name>.<version>, with the tape's own file timestamps preserved in tree.txt and a catalogue in mdl-files.csv. It holds the interpreter's sources (mudsys), three generations of compiler (compil, ncompi, xcompi), the assembler and the small programs (mprog), the debugging tools (mudbug), the library (libmud), the pure code databases (mudsav), saved images, and forty-one of the compiler's own .record files. Its preferred citation is in Appendix G; its licence is MIT's.

The tapes this book cites most, by their archive numbers:

tape reel date what pymdl takes from it
7005458 1981-08-03 TS MUDDLE release 55; the xcompi compiler sources pymdl's compil/ descends from; the debugging tools; MUDSAV;SQUOZE TAB55, the release symbol table
9006255 1983-08-04 the ncompi compiler binaries (38 FBINs), SAV FILE and FIXUP FILE, the library, MPROG sources including the SQUOZE package and TIMFCN; the ITS root the era sessions run on
9006231 1983-05-17 the assembler source (mprog/assem.undr88)
9006260 1983-03-02 CLR; helper binaries (atosq.nbin, mudhak) and the BTB; opcode tables
7005401 1983-08-02 an earlier compil tree: atosq.ucr005, the era's own assembly source; ops.vec
2100099 1981-09-03 the April 1981 compiler sources, the coherent late set closest to the ncompi binaries
100332 1982-12-29 connect-dir.mud; a 106-series compiler save
9005183, 9000679 1980, 1982 interpreter and compiler helpers the tests load

A second archive, ~/muddle, is the TOPS-20 side: <mdl.comp> with the compiler's load file pcomp.load.13, and the 1977 manuals. Both are read and never written: the rule for every file in them is that pymdl copies, it does not edit, and where a copy had to change the change is recorded (38.4).

38.2 What pymdl carries, and where

MDL source, in the package. src/pymdl/mdl/ holds 116 files of the era's MDL -- the compiler (60), the debugging tools (37), the library system (7), the small programs (7), the two symbol tables and three loose programs -- each a copy of an archived original except where MANIFEST.tsv says otherwise. This is what pymdl runs interpreted: chapters 29, 30, 34 and 35. The library directory mdllib/ beside them is a build output, the installed copies tools/build_mdllib.py makes by running the era's own librarian over those sources (chapter 30), and nothing in it is tracked.

Era binaries, for the tests. tests/data/era/ mirrors the archive's layout: 109 files, 2.7 megabytes, every one either a copy -- an FBIN, NBIN, SAVE or symbol table, byte for byte, which nothing can regenerate because MIT's compiler emitted it -- or an extract: the pure-code database MUDSAV;SAV FILE is 11 megabytes and its FIXUP FILE three, and the compiler's FBINs reference forty-four blocks between them, so the vendored database holds just those blocks. tools/vendor_era_data.py --build refreshes what is vendored from the archive and writes the manifest; --verify asks the archive whether every copy still matches, and skips when there is no archive. Vendoring is what let the suite stop skipping on a machine without the tapes -- before it, the suite reported green while testing a fraction of what it claimed.

Oracle corpora. tests/data/oracle/ holds 710 self-contained MDL expressions written to be asked of MDL 55 and of pymdl, and a golden file of pymdl's own answers. That golden file compares pymdl against itself: it cannot say pymdl is right, only that it has not changed silently across 710 forms; the era side is re-derived by the oracle test of chapter 37 when it runs.

One saved world. tests/data/madadv-1977.save is MADMAN;MADADV SAVE from the last MIT-DDC dump, December 1977: a saved interpreter with a game in it, read as an object world by mdlsave.py (38.3).

38.3 The formats

36-bit words in 8-bit bytes. Every binary came through Bawden's evacuated format (36.6): five 7-bit bytes for a text word, a five-byte escape otherwise. its36.py decodes and encodes it and is the reader under every other format here.

Object files. NBIN, FBIN and GBIN are chapter 33's: MDL text with a binary portion for the RSUBRs (33.11, 33.12).

The pure-code database. SAV FILE holds the purified code blocks a whole GROUP-GLUEd package shares, FIXUP FILE their chapter 33 fixup records, and an FBIN keeps no code of its own: its RSUBR's first slot is a read-time %<PCODE "name" offset> into a block (33.13). The record used to say those binaries were unreadable, that "the machine words live in the pure database and do not resolve in a bare image"; tools/era_tape_code.py is the proof that they resolve, recovering MIT's own compiled words per function straight off the tape.

Found along the way. A pure block is the output of the compiler generation before the one it contains -- ncompi's binaries were compiled by the compiler before ncompi -- so a byte difference between our compiler's words and the tape's can be version drift rather than a defect, which is why the oracle of 34.8 compiles our source on MDL 55 instead of diffing against the tape. tools/era_tape_diff.py does diff against the tape, and has to account for four kinds of difference the same source compiled the same way must show: an M-relative offset (package base there, function base here), an index into a shared reference vector, a call to a sibling that is a direct PUSHJ when glued and an MCALL when standalone, and pooled constants. Only a word outside those four is unexplained, and an unexplained word is either a real divergence or a source-revision difference, which is the whole question.

Saved images. SAVE (chapter 14) wrote the interpreter's core, and mdlsave.py reads one back, format from mudsys/save.mcr159: word 0 the version string (54 , 55 ), word 3 the top of garbage-collected space, words 5 onward memory at their own addresses, then the purified pages. The interpreter's high segment -- its code, and every built-in atom that lived there -- is not in the file; it was shared from the TS MUDDLE executable, so pointers above the pure region resolve to nothing. What is there is walked with the in-core layouts of muddle.mcr324 and atomhk: a value is two words, [type,,flags][value]; the type vector is pairs indexed by type code (the census of 35's 7.1.7); an atom is a value cell, a link word and an ASCIZ name.

Measured (tests/test_mdlsave.py). The December 1977 MADADV save is release 54, its purified region starts at 502000, it holds sixteen oblists (the package system's) and more than 1,800 atoms, among them LIVING-ROOM, DIAGNOSE, CAROUSEL-OUT. The four surviving saved games order themselves by atom count: about 960, then 1,220 (July 1977), then this one at 1,608 -- the ROOM-type rework, the volcano, the balloon.

GC-DUMP files. GC-DUMP and GC-READ (chapter 14, the manual's 11.5) promise to reproduce an object "exactly, including sharing". The PDP-10's format was a relocated copy of the garbage-collector graph; pymdl's is its own byte stream, one entry per backing store, so REST-sharing, shared tails and cycles round-trip, and it survives a PRINTB channel. The restrictions are the manual's and were measured: MDL 55 refuses a FRAME, a LOCD and a TUPLE with STRUCTURE-CONTAINS-UNDUMPABLE-TYPE, naming the type. pymdl does not read the era's GC-DUMP files; none survive that anything here needs.

Symbol tables. MUDSAV;SQUOZE TAB55, the release's export of its DDT symbols, is carried as text in octal (mdl/mudsav/squoze.tab55.txt), verified word-identical to the tape's binary, and ddt.syms55 is the fuller table -- globals and local labels -- extracted from MIT's own dumped image. The assembler, the emulator and the debugger all answer from them (35, 36). The BTB; opcode tables ops.vec and ops.ovec are transcribed the same way.

Records. The compiler's .record files (34, 5.1.1) survive on the tapes, forty-one of them, and are what a compilation's messages looked like; pymdl's FILE-COMPILE writes the same kind of file.

38.4 Provenance

A copy can drift, be quietly edited, or be a stand-in that only looks like the original, so provenance here is derived, never asserted. tools/mdl_manifest.py matches every file under src/pymdl/mdl/ by content against both archives, whitespace normalised and evacuated files decoded first, and writes MANIFEST.tsv: the archived original, its era date, a hash and a note. A file that matches nothing is pymdl's own -- a stand-in, an extract or a conversion -- and keeps the note it had. --check fails when the manifest is stale; it is run when material is vendored, not on every test run, because it needs the archives. tests/data/era/MANIFEST.tsv does the same for the binaries, with --verify against the tape.

Found along the way. Two things a search by filename cannot catch, and a content match did. An ITS first name is six characters, so bitsgen is bitsge.ucr002 on the tape and compdec is compde.ucr030, and both were once recorded as having no ITS source at all. And a file can live inside another: POPWR2's DEFINE was found inside cargen.mud, and the era's SQUOZE package vendored today matches 99.4% of 9006231/mprog/squoze.undr04 -- the remainder being five control characters and the letters COU after <ENDPACKAGE>, a tape artifact the era's own librarian refuses (it interns an atom on INITIAL), so the copy stops at the end of the package and the manifest says so. The match has a floor: a 27-byte stand-in once "matched" inside a binary, a false provenance, so a substring shorter than 200 characters means nothing.

Provenance is also sticky: once a file has been matched to one original, a later run does not silently move it to another tape that happens to hold the same text, because the archive holds many identical copies and which one is named should not depend on directory order.

For the compiler the record goes further, because its source was changed (34.10). compil/CHANGES.md carries every diff against the archived original and the measurement that justified it, and four instruments watch the source's standing against the archive: era_source_audit.py checks our functions' names and #DECLs against the ~780 signatures carried by MIT's own FBINs; era_source_outliers.py reports the functions where every archived revision agrees and ours alone differs, which is the shape the CSTORED-SLOT defect had; era_source_ab.py swaps one module's source for another archived revision and re-measures, the one arm of a tape difference that can be manipulated; and era_tape_code.py with era_tape_diff.py compares words. Their standing finding is worth stating plainly: pymdl's compil/ descends from the xcompi tree, which is the worst match to MIT's shipped binaries among the revisions the archive holds, and the restorations in CHANGES.md are what closed the gap.

Found along the way. Copies rot on the tape too. Nine of the ten surviving copies of prcod.nbin descend from one damaged master with the low bit of every word zeroed: PUSH TP,1(AB), ASH A,1 and MOVSI A,1 collapse to their zero-address forms, the type-vector name in the fixup record stops resolving, and a fixup location slides off its JRST onto the instruction before it. The 7005401 copy is the intact original -- its words match the assembler source primhk.1 and its fixup locations land on the reference sites -- so that is the one staged and vendored, and the nine "byte-identical" copies agree only with each other. And 100332/compil/atosq.nbin.2 is an older binary format the loader does not read, which is why the ATOSQ reconstruction of chapter 35 was checked against the four newer binaries and not five.

38.5 What is not here, and why

  • The interpreter. mudsys, MIT's MIDAS source for MDL itself, is not vendored and never runs here: pymdl is the interpreter, and the sources are read as evidence (eval.mcr111, readch.mcr187, save.mcr159 are cited by line throughout the code) rather than executed.
  • The games. Zork, in all its dated trees, is a program pymdl runs and lives in its own repository; the game corpus that once sat under src/ was removed on 2026-08-28 and a synthetic program stands in for it in the tests.
  • The simulator. apsim, the KL10 and KA10 builds, and the ITS build that hosts MDL 55 are separate projects (chapter 37); pymdl points at them and skips without them.
  • Most of the tapes. Of the archive's thousands of files pymdl carries 116 sources and 109 binaries. Everything else is reached, when a measurement needs it, through PYMDL_ERA_CORPUS, read-only.