The pymdl book
37. Asking the era
The manuals do not settle every question about MDL. Some behavior is unspecified; some archived programs belong to a different revision; and some printed claims disagree with the surviving interpreter. The project uses MDL 55 under apsim to investigate those cases.
This chapter explains how to ask a reproducible question, compare the answer with pymdl, and preserve the result as a test. It also describes limitations of the instruments: a simulator can return a plausible answer without reproducing the operating-system behavior being investigated.
When a claim concerns MDL 55, prefer a controlled observation to an assumption. Record the release, inputs, result, and relevant test. A measurement on release 55 does not by itself establish behavior on release 105 or on every archived revision.
37.1 What "the era" is, here
MDL 55. MIT's interpreter, release 55, is the ITS TS MUDDLE on the
August 1981 tape (7005458/mudsav/ts.mud55), and it boots under pdp10-apsim,
a PDP-10 and ITS simulation from the pdp10-xdev toolchain outside this
repository (PYMDL_APSIM). It runs on a writable copy of an ITS root,
because MDL writes into its disk -- RESTORE, PRINTB, GC-DUMP -- and the
copy is of the 1983 tape 9006255, 37 megabytes, kept under
~/scratch/pymdl/era-root (PYMDL_ERA_ROOT). Use a persistent working
directory: these sessions write files that later measurements may need.
The same directory holds the saved compiler images ecomp.55save,
pcomp.55save and npcomp.55save, which a session can RESTORE to become
the compiler of chapter 34.
cp -r ~/mdl-1971-1987/mdl/9006255 ~/scratch/pymdl/era-root
chmod -R u+w ~/scratch/pymdl/era-root
MDL 55 from source. A second MDL 55, assembled from MIT's own sources by
the user in a provenance-controlled tree (~/mdl/src/mdl55, PYMDL_MDL55),
is what tests/test_mdl55_oracle.py asks. Its driver, ask.py, lives with
that tree and has moved twice; the test walks up from the tree to find it
rather than encode a path that rots.
The simulator is not the oracle. apsim is itself an unverified program where MDL's behaviour depends on the operating system, and 36.5 has the case that cost three sessions: its terminal calls are stubs. An answer that depends on ITS is confirmed against the ITS and MDL sources in the archive before anything is changed to match it.
37.2 Three instruments
They do not overlap, and each is shaped by what it can and cannot see.
Values, in batch. tests/test_mdl55_oracle.py writes a list of
expressions into MUDSYS;MUDDLE INIT, boots MDL 55 with no terminal, and
reads back what each printed; pymdl is asked the same expressions and the two
must never disagree. About 122 expressions in under a second, part of the
quick gate, and usable by hand for one:
PYTHONPATH=src python3 tests/test_mdl55_oracle.py -e '<FIX -3.7>'
It never compares the wording of an error, and it cannot ask anything stateful, because the driver it uses abandons the rest of the file on an error and starts a fresh interpreter (37.4). It skips, and says which piece is missing, when apsim or the tree is not there; it used to be run by hand, which is how it rotted three ways at once without anything noticing.
Banners, one boot per form. tests/era_battery/ is one instrument in
four parts. Five generators write forms -- 720 unique -- of five kinds:
illegal calls at the documented arity (259), legal calls and what the 55
answers (134), two mechanisms meeting (112, the cases the other two are
blind to by construction), one argument too many from a legal baseline
(66), and the same without a baseline (151), using the finding that the 55
checks the argument count before the types, so a row of zeros one past
the documented maximum asks the arity question by itself. The sweep boots
the era interpreter per form, about six seconds each, so the whole battery
is over an hour; the post-processors print a table and emit entries for
subrs/argcheck.py with the error atom taken from the oracle, because
adding an argument check by hand means guessing which atom the 55 names, and
this project was wrong about that often enough to build a machine to stop.
This is the only instrument that can see that the 55 says
FIRST-ARG-WRONG-TYPE where pymdl said ARG-WRONG-TYPE. A battery row is a
lead, not a finding: some of what it flags is the era's own quirk, and four
rounds were spent learning that.
A session, form by form. tools/era_session.py is the era terminal:
boot the ITS image, wait for the listener, perhaps RESTORE a compiler, then
type forms and read each answer back, in one interpreter that keeps its
state. Four tools carried their own copy of that driver until it was made
the one copy; era_oracle's collect (34.8), era_saves (the same case
compiled with every archived compiler image, to tell an engine property from
a revision property), mdl55_errhandler (the error handler's behaviour:
which level you land at, whether a nested error stacks, what a bare ERRET
does), and the one-off probes that measured FLOAD's INCHAN (chapter 2),
REMOVE's new atom (chapter 18) and, today, ALLTYPES (chapter 35) all run
through it.
$ PYTHONPATH=src python tools/era_session.py --save ECOMP '<1 <ALLTYPES>>' '<CHTYPE <TYPE-C LOSE> FIX>'
<1 <ALLTYPES>> -> LOSE
<CHTYPE <TYPE-C LOSE> FIX> -> 0
Found along the way, driving the terminal. Each fact below cost a wrong measurement first. MDL 55 activates a form on ESC, not newline: a newline-fed session sits at
LISTENING-AT-LEVEL 1having read nothing, which a harness once reported as "the era evaluated nothing". Every form is followed by a second form thatPRINCs a unique marker, built as<STRING "@@K" "n@@">so its text never appears in an echoed input line, and the answer is everything up to the marker.<RESTORE>flushes read-ahead console input, so its marker would be eaten; the driver waits for the compiler's ownNOW READYinstead.<RUNTIMER 30000>goes first, because the default is "about 8 cpu minutes" and a corpus run is longer. A secondDEFINEof a name errors unless<SET REDEFINE T>. An error drops the listener toLISTENING-AT-LEVEL 2and every later answer is suspect, so the level is checked after each form and unwound with<ERRET>. Long values come back folded at 80 columns unless the init file widens the channel,<PUT ,OUTCHAN 13 10000>. And apsim simply goes away partway through a large corpus -- at case 216 of 369, once -- so the collector restarts a session on what is left, which is why a 369-case run still completes in under a minute.
37.3 Three ways to measure nothing
An instrument that can report agreement without having looked is worse than no instrument, and this project has built several. They are kept in the record because the shape recurs.
- Fresh interpreters that look like one. Asked through the batch driver,
<+ 1 FOO><+ 2 BAR><+ 3 BAZ>answersLEVEL 2three times, which reads exactly like "MDL 55 never stacks levels" and is three fresh interpreters each erring once. Two versions of the error-handler comparison were built on that reading and measured nothing; the second found eleven differences that were all the oracle restarting. The third feeds forms to the terminal, ESC-terminated, in one session, and found that levels are counted per process and the banner names the process. - A comparison of one field. The first apsim instruction differential
(36.1) reported "354 opcodes swept, 0 differing". Its key normaliser
matched
'ac 'where the parser produced'ac0', so every case compared the program counter alone. It now refuses to run unless at least seventeen fields compare. - A configuration that could not run.
EC_RAW=1was read only inside a function the driver never called, so the authentic path passed its fourteen samples for weeks while running the default build under another name (34.7.3). The gate now demands evidence that the staging happened, not only the score; and a configuration whose sources are absent skips and says so, because a skip is a statement that something could not run, not that it would pass. - A stub that looked like a fact. apsim's
TTYGETreturns zeros, which happen to describe a particular terminal, and three sessions reasoned about a case-folding difference from them before the cause turned up in apsim's own input path (36.5).
The countermeasures are ordinary and are used: controls that must fail
(era_refusals aborts unless ADD1 compiles the same and DIVG compiles
differently, 34.11), mutation testing of a probe before believing its
verdict (the error-handler probes were mutation-tested twice), asserting the
answer of a compiled case and not only its word count (34.8), and naming
the machine a floating-point result was measured on, because a KS10 and a
KL10 answer differently (36.1).
37.4 From a measurement to a line in this book
The chain has four links and every Measured box in this book has walked them.
- Ask. A question the manual does not answer, or a divergence a test
or a program surfaced, is put to MDL 55 as forms, through whichever
instrument fits: a value in batch, a banner, a stateful session. The
35th chapter's
ALLTYPESquestion was one form and one answer. - Pin. The answer becomes a test in
tests/, with the date and the image it was measured on, so the engine cannot drift from it silently. Many tests' docstrings begin with the measurement; the harvest (below) reads them. - Change, and record. The engine is made to agree. Where the change is
to an era source pymdl carries -- a bug in the compiler's own MDL -- the
fix goes into the vendored file and its diff into
compil/CHANGES.md(34.10), and any oracle row it moves is marked as differing by design. Where the answer is an era program's own property rather than the interpreter's (theCELESTdeclaration of chapter 2), it is documented and left. - Write. The box says what was measured, when, on which image, and what pymdl does about it. A box begins Measured when it pins a fact the manual states or implies, Found along the way when the manual is silent and the fact was discovered doing something else, pymdl's own when there was no era behaviour to measure. Appendix E gathers them.
tools/book_harvest.py exists so step 4 is selection rather than
rediscovery: it collects every "MDL 55, measured" comment in the source,
every test docstring that states a measurement, the tools' docstrings, the
CHANGES.md sections and the commits that record a measurement into one
working file (docs/pymdl/harvest.md), each tagged with a first guess at the
chapter it belongs to.
37.5 What cannot be asked
- The Tenex personality. MDL 105's manual describes it (chapter 3), and a TOPS-20 MDL 104 exists outside this repository, but no harness here drives it; the Tenex behaviours in this book are the manual's, and are marked so.
- What the image does not have. A bare MDL 55 has no
DSKDATE-- 76 of pymdl's 426 default globals are absent from it, of which only four are ever probed for by era code -- and noACTIVATE-CHARS; a question about those is a question about the library or the game, not the interpreter. - The operating system. Where the answer depends on ITS, apsim's answer is checked against the ITS and MDL sources before it counts (37.1).
- Time. A full oracle collection, a full self-host tree run and a full
tape diff are hours each and disturb the machine they run on; they are run
when someone decides to, and the checked-in answers (
tools/era_oracle.tsv, the numbers intools/era_selfhost.py) carry their dates so a reader knows what they were measured against. A targeted case --python tools/era_oracle.py collect W1 ZO-- is cheap and is the normal way.
37.6 Running it yourself
| piece | where | knob |
|---|---|---|
| the tape archive | ~/mdl-1971-1987/mdl |
PYMDL_ERA_CORPUS |
the second archive (TOPS-20 compiler sources, pcomp.load.13) |
~/muddle/<mdl.comp> |
PYMDL_ERA_MUDDLE |
| apsim | ~/pdp10-xdev/bin/pdp10-apsim |
PYMDL_APSIM |
a writable ITS root with mudsav/ts.mud55 and the .55save images |
~/scratch/pymdl/era-root |
PYMDL_ERA_ROOT |
MDL 55 built from source, with ask.py |
~/mdl/src/mdl55 |
PYMDL_MDL55 |
Every test that needs one of these skips without it and names what is
missing. The tools that ask the era are listed in tools/README.md; the
one to start with is era_session.py, and the one question to ask first is
the one you were about to reason about.