The pymdl book
Appendix E. Measured differences and discoveries
Hand-written, from the "Found along the way", "Measured, MDL 55" and
"pymdl's own" boxes in the chapters, and from docs/pymdl/harvest.md (the
working file that gathered the raw material out of code comments, test
docstrings, CHANGES.md and commit messages).
This appendix exists because the same two questions come up on every page of this book and are answered a paragraph at a time: where does pymdl deliberately differ from MDL 55, and what did this project learn about MDL 55 that the manuals do not say? The chapters answer them in place, where the reader meets them. This is the only place they are listed together.
Three things this appendix is not. It is not a bug list -- a genuine defect is a thing to fix, and the ones found while writing this book were fixed, with tests, and are recorded below only as discoveries. It is not a feature comparison -- pymdl is not trying to beat MDL 55 at anything. And it is not exhaustive about the machine: chapter 36 documents the emulator's own divergences from real hardware at their own sites, because they are answerable only against a specific CPU.
E.1. What "measured" means
A claim in this book marked Measured, MDL 55 was obtained by asking the
question of a real MDL 55 running under an emulated PDP-10 and asking the same
question of pymdl, and comparing the answers. Chapter 37 is how that is done;
the short version is that tools/era_session.py drives one long-lived MDL 55
through its own terminal, and tools/mdl55_errhandler.py installs an "ERROR"
interrupt handler (chapter 23, section 21.8.11) so that a question with a wrong
answer does not end the session.
Three batteries of such questions are kept:
| what | size | where |
|---|---|---|
| the oracle corpus -- expressions asked of both | 710 forms | tests/data/oracle/corpus*.mdl |
| the error battery -- generated forms chosen to provoke banners | about 720 forms from five generators | tests/era_battery/ |
the compiler shape oracle -- FUNCTIONs compiled both ways |
640 shapes | tools/era_oracle.tsv |
Those sizes are checkable: the golden file has one line per corpus form, the
shape oracle one non-comment line per shape, and tests/test_comprehensive_bars.py
runs the battery's generators.
The oracle corpus is compared against a golden file of pymdl's own answers,
not the era's: it is a regression net proving pymdl has not silently changed,
which is a weaker and more honest claim than proving it right. Re-deriving the
era side needs the emulator, and tests/test_mdl55_oracle.py is what does it.
E.2. Deliberate differences
Each row is a place where pymdl answers differently from MDL 55 on purpose, because the era's answer depended on a machine or an operating system that is not here. Each names the chapter where it is explained.
E.2.1. The substrate
| difference | pymdl | MDL 55 | chapter |
|---|---|---|---|
| Compiled code | Python functions (PYCODE), or era PDP-10 code under the emulator |
PDP-10 machine code | 33, 34 |
A SUBR |
a Python function | assembled PDP-10 code in the interpreter | 6 |
| Recursion depth | Python's stack, raised at start-up, with an evaluator guard that reports runaway recursion as an MDL error | the control stack, which GROWs |
13 |
| Arithmetic width | 36-bit FIXes carried through +, -, *, /, so <OVERFLOW <>> still wraps |
the hardware | 19, 21 |
FLOAT |
a Python float, except that CHTYPE between FIX and FLOAT reinterprets a genuine 36-bit word |
PDP-10 single precision | 9 |
The FLOAT row is the one with a seam in it, and chapter 9 has why: <MIN> and
<MAX> must CHTYPE to the extreme FIXes, because Zork's "biggest number"
idiom depends on it.
E.2.2. Storage
| difference | pymdl | MDL 55 | chapter |
|---|---|---|---|
| The garbage collector | Python's; GC, BLOAT, BLOAT-STAT and GC-MON all exist and answer the documented shapes, about pymdl's own storage |
a copying or mark-sweep collector over five address spaces | 24 |
GROW's collection |
not reproduced; the 32/64 rounding is | "requires a garbage collection every time" | 10 |
| Associations | held strongly, so one survives until removed by hand | collected with their item and indicator | 16 |
An escaped TUPLE |
still readable, but LEGAL? answers #FALSE () and printing refuses with ILLEGAL-ARGUMENT-BLOCK |
read whatever later frames pushed into those stack words | 12 |
PURIFY, SUBSTITUTE |
fully reproduced -- chapters 31 and 33 depend on them | -- | 24 |
The TUPLE row is the pattern the whole section follows: pymdl reproduces every
honest observable and declines to reproduce undefined behaviour. A program
that obeys the manual cannot tell the difference; one that does not gets a
definite answer instead of the era's arbitrary one.
E.2.3. Files, terminals and other processes
| difference | pymdl | MDL 55 | chapter |
|---|---|---|---|
| File names | ITS syntax understood and reported ("DSK:name second"), resolved against a host file root |
real ITS directories | 14 |
SAVE files |
pymdl's own format, restoring the same observables; an era SAVE file is refused rather than misread |
an image of the address space | 14 |
GC-DUMP files |
pymdl's own format; sharing and self-reference preserved, as promised | an image of storage words | 14 |
Terminal SUBRs |
present and sensible; a pymdl session is as often a pipe, and TYI reads without waiting for ESC |
a real terminal with ITS interrupt characters | 14, 23 |
The "NET" device |
OPEN answers a FALSE |
ARPANET sockets | 14 |
IPC (SEND, IPC-ON, DEMSIG) |
present; answer the documented "nobody is listening" FALSE |
ITS inter-process messages | 25 |
ACTIVATE-CHARS |
absent (the manual marks it Tenex-only) | Tenex and Tops-20 only | 23 |
| "Dangerous" interrupts | do not occur; pymdl's faults arrive as ERROR |
PDP-10 hardware traps | 23 |
LOGOUT, VALRET |
answer #FALSE (), the documented "if it does return" case |
talk to the superior process | 25 |
An era SAVE file is not lost, only read elsewhere: chapter 33 reads one,
because the emulator has an address space to read it into.
E.2.4. Numbers and defaults that differ
| difference | pymdl | MDL 55 | chapter |
|---|---|---|---|
.OBLIST in a fresh interpreter |
four entries -- INITIAL, ROOT, and the private oblists of the preloaded PPRINT and EDIT packages |
two, plus whatever was preloaded (section 15.5 allows exactly this) | 18 |
| Channel numbers | pymdl's own | ITS channel numbers, so the manual's <FOO ,INCHAN> is 1 |
17 |
RANDOM |
MDL's own generator, ported instruction for instruction, so the default sequence matches | the same generator | 6 |
GVAL/LVAL interference (section 24.4) |
absent; alternating costs nothing | a cache in the atom block | 26 |
TIME |
this process's CPU seconds | the MDL job's | 25 |
E.2.5. Known gaps
These are not departures; they are places where pymdl is simply not finished. Each is recorded at its own site in the chapter as well.
| gap | effect | chapter |
|---|---|---|
The "READ"/"WRITE" monitor's third argument |
a FRAME by TYPE that prints as #FRAME PUT, so the manual's own example works; but FUNCT and ARGS reject it as FIRST-ARG-WRONG-TYPE |
23 |
SETLOC through a LOCD |
does not fire a "WRITE" monitor set on that locative |
23 |
| Native compilation coverage | 413 of 439 library FUNCTIONs (94.1%); the refusals are honest FALSEs naming the reason |
34 |
E.3. Discoveries
Things this project measured or worked out that neither manual states. They are grouped by where they live, and each names its chapter; the chapter has the evidence.
E.3.1. About the language, from MDL 55 itself
| discovery | chapter |
|---|---|
An ATOM's value cell belongs to the atom, not its PNAME: FOO and FOO!-INTERRUPTS are different variables with different values |
7, 18 |
T and ELSE have no value at all -- they are unbound ATOMs that evaluate to themselves, which is the whole of why COND clauses can use them |
11 |
SET, LVAL, VALUE, UNASSIGN and LLOC reject a non-ENVIRONMENT in their final argument rather than ignoring it, and SET's banner says SECOND though the argument is its third |
7 |
UNASSIGN leaves the binding and removes only the value, so BOUND? still answers T |
7 |
EQVB folds left rather than complementing the exclusive-or of all its arguments; the two agree at two arguments and part company at three |
21 |
<ASCII -1> is ARGUMENT-OUT-OF-RANGE, not the appendix-4 name BAD-ASCII-CHARACTER |
6 |
A CLOSURE prints its captured bindings: #CLOSURE (#FUNCTION (() .A) ((A 1))) |
12 |
INITIAL has 151 hash buckets and ROOT has 317 |
18 |
PICTURE is a release-55 built-in neither manual documents; <TYPEPRIM PICTURE> is STORAGE |
9 |
^O is ^S only off ITS; under ITS a typed ^O does nothing, which section 16.8's parenthesis says and nobody reads |
19 |
The era's initial ACTIVATE-CHARS set is ordered ^G ^S ^O, not sorted |
23 |
The argument checking is irregular per-SUBR in ways no rule predicts; the table of 720 measured forms is the only description |
6, 19 |
mudsys/primit carries ILLEGAL-ARGUMENT-BLOCK, ILLEGAL-FRAME and ILLEGAL-LOCATIVE as a matched trio, one for each thing that dies when a Function returns |
15 |
A FUNCTION application has no FRAME of its own; the EVAL of its calling form stands for it |
19 |
MDL 55's own <AND> and <OR> with no arguments compile to code that will not run |
11, 34 |
E.3.2. The manuals' own errors
Roughly thirty places in the two manuals state something the interpreter
contradicts -- arithmetic that does not add up, brackets that do not balance,
a DECL that would stop its own example from running. They are corrected in
place with ~~strikethrough~~ and the evidence, not gathered here, because a
reader meets them while reading the passage and not while consulting a table.
Chapter 12's INC1, chapter 13's LNUM, chapter 16's PERCENT association and
chapter 18's INC are the four where the manual's own example does not run
until the correction is applied.
E.3.3. About the era's own programs
| discovery | chapter |
|---|---|
MIT's newlf.mud ends FIND-FILE with (#FALSE ("File not found")) -- a deliberate reason carried by a one-element COND clause, which is what caught pymdl's COND discarding reasons |
14 |
% and %.INCHAN are used by era sources to compute parts of a file as it is read, so the reader must evaluate with a live interpreter and the real input CHANNEL |
20 |
The <AND <OPEN ...> ...> idiom of section 8.4.1 appears in the corpus as OR: mat.mud turns a FALSE into its own error, inquire.mud falls back to a second directory |
11 |
EXP-MAC, the compiler's peephole optimizer, is itself a MACRO expander |
20, 34 |
Two analyzers in symana.mud carry bugs their sibling copies lack |
34 |
The compiler's FILE-COMPILE is a different thing in an era compiler image than in its source |
34 |
The assembler moves the atoms *INSERT, ARG and others between oblists, which is what destroyed pymdl's SQUOZE shell until the era's own MDL package replaced it |
35 |
| Nine of the ten copies of one file on the tape have rotted; the tenth is intact | 38 |
MDL's own mudsys makes 23 distinct .CALLs |
36 |
E.3.4. Bugs this book found
Writing a chapter means running its examples, and running the manual's examples found defects no test had. Each was fixed with a test; the chapter carries the story.
| defect | found by | chapter |
|---|---|---|
The printer chose brackets from the Python class rather than the TYPE, so one value printed two ways depending on how it was made |
<CHUTYPE .LOST LIST> in section 7.6.5.2 |
10 |
The #TYPE prefix path had the same defect, so an "ARGS" LIST CHTYPEd to FUNCTION printed #FUNCTION <...> |
FUNCT1 in section 9.5 |
12 |
COND returned a fresh empty FALSE instead of the last FALSE it saw, discarding every reason a failing test carried |
COUNT-CHAR in section 11.3, and its own comment |
14 |
SET never consulted a DECL placed on a top-level binding by PUT-DECL, which is the situation PUT-DECL is documented for |
section 14.7.3 | 17 |
A dead locative and an out-of-range AT answered pymdl's own English rather than era banners |
section 12.1.1 | 15 |
The native COMPILE leaked a Python exception on a QUOTEd parameter instead of answering a FALSE |
section 9.4 | 34 |
FILE-ASSEMBLE died TYPE-MISMATCH because MESSAGE-CHANNEL was set at the wrong time |
chapter 35's E7 | 35 |
ALLTYPES was not in MDL 55's measured type-code order, so the unassembler named FIX's code FLOAT |
chapter 35 | 35 |
E.4. Where to look next
- Chapter 37 is how the measuring is done, and is the chapter to read before adding a row to any of these tables.
docs/pymdl/harvest.mdis the working file: 341 hits from code comments and docstrings, 19CHANGES.mdsections and 59 commit messages, each tagged with the manual section it belongs to. It is raw material, not prose, and it is where a claim's original wording lives.src/pymdl/mdl/compil/CHANGES.mdrecords every change made to MIT's vendored compiler sources, with the diff and the reason; appendix G has the rest of the provenance.