The pymdl book
33. Compiled programs and their files
A compiled MDL program is a PDP-10 machine-language program written to run in the MDL environment. This chapter is about the objects it becomes and the files it lives in. How it is produced is chapter 34 (the compilers) and chapter 35 (the assembler); what runs it is chapter 36 (the PDP-10 inside). Everything here holds whether or not that machine is switched on, except the last step -- executing the words -- and the chapter says where that line falls.
19.1 RSUBR (the TYPE)
RSUBRs ("relocatable subroutines") are machine-language programs written
to run in the MDL environment. They are usually produced by the MDL
assembler (often from output produced by the compiler) although this is not
necessary. All RSUBRs have two components: the "reference vector" and the
"code vector". In some cases the code vector is in pure storage. There is
also a set of "fixups" associated with every RSUBR, although it may not be
available in the running MDL.
pymdl has all three components as the era had them, and one more that the
era did not need: for an RSUBR whose name the interpreter knows a Python
peer for, the peer answers in place of the code (33.14).
19.2 The reference vector
An RSUBR is basically a VECTOR that has been CHTYPEd to TYPE RSUBR
via the SUBR RSUBR (see below). This ex-VECTOR is the reference
vector. The first three elements of the reference vector have predefined
meanings:
- The first element is of
TYPECODEorPCODEand is the impure or pure code vector respectively. - The second element is an
ATOMand specifies the name of theRSUBR. - The third element is of
TYPEDECLand declares the type/structure of theRSUBR's arguments and result.
The rest of the elements of the reference vector are objects in
garbage-collected storage that the RSUBR needs to reference and any impure
slots that the RSUBR needs to use.
When the RSUBR is running, one of the PDP-10 accumulators (with symbolic
name R) is always pointing to the reference vector, to permit rapid access
to the various elements.
<SET R <RSUBR [#CODE ![0!] FOO]>> ⇒ #RSUBR [#CODE ![0!] FOO]
<TYPE .R> ⇒ RSUBR
<LENGTH <CHTYPE .R VECTOR>> ⇒ 2
<TYPE #CODE ![1 2!]> ⇒ CODE
<TYPEPRIM CODE> ⇒ UVECTOR
Found along the way. The third element is not guaranteed. The era assembler emits
[code name]for a routine that carries noDECL, and MDL 54 loads it: the 1977TAA;TELLB NBINholdsRSUBRs of length 2 (GET-NAME), 3 (DSKDATE,GXUNAME) and 8 (TELL), measured under apsim 2026-08-29. SoRSUBR(the SUBR) requires two elements, not three.
19.3 RSUBR linking
RSUBRs can call any APPLICABLE object, all in a uniform manner. In
general, a call to an F/SUBR is linked up at assembly/compile time so that
the calling instruction (UUO) points directly at the code in the interpreter
for the F/SUBR. However, the locations of most other APPLICABLEs are not
known at assembly/compile time. Therefore, the calling UUO is set up to
point at a slot in the reference vector (by indexing off accumulator R).
This slot initially contains the ATOM whose G/LVAL is the called object.
The calling mechanism (UUO handler) causes control to be transferred to the
called object and, depending on the state of the RSUBR-link flag, the
ATOM will be replaced by its G/LVAL. (If the call is of the "quick"
variety, the called RSUBR or RSUBR-ENTRY will be CHTYPEd to a
QUICK-RSUBR or QUICK-ENTRY, respectively, before replacement.)
Regardless of the RSUBR-link flag's state, calls to FUNCTIONs are never
permanently linked. A call to a non-Subroutine generates an extra FRAME,
whose FUNCT is the dummy ATOM CALLER.
RSUBRs are linked together for faster execution, but linking may not be
desirable if the RSUBRs are being debugged, and various revisions are
being re-loaded. A linked call will forever after go to the same code,
regardless of the current G/LVAL of the called In pymdl no call is
ever permanently linked: the UUO handler resolves the ATOM.ATOM in the
reference-vector slot through its G/LVAL on every call, so a re-DEFINEd or
re-loaded callee is always the one reached.
On the original MDL, callers could disable linking while testing RSUBRs
by calling RSUBR-LINK with a FALSE argument, and enable it with a
non-FALSE argument. pymdl retains the flag-setting interface, but the
flag does not change call dispatch. A call with an argument returns the
previous flag value; a call without arguments returns its current value.
<RSUBR-LINK> ⇒ T
<RSUBR-LINK <>> ⇒ T
<RSUBR-LINK> ⇒ #FALSE ()
pymdl's own.
RSUBR-LINKkeeps the flag and answers as the manual says, and nothing consults it: there is no faster path for a linked call to take.QUICK-RSUBRandQUICK-ENTRYexist asTYPEs, with the era's type codes 53 and 54, so a reference vector that holds one reads and prints; applying one applies the RSUBR it names.
19.4 Pure and impure code
The first element of an RSUBR is the code vector, of TYPE CODE or
PCODE. TYPE CODE is of PRIMTYPE UVECTOR, and the UTYPE should be
of PRIMTYPE WORD. The code vector is simply a block of words that are
the instructions which comprise the RSUBR. Since the code vector is stored
just like a standard UVECTOR, it will be moved around by the garbage
collector. Therefore, all RSUBR code is required to be
location-insensitive. The compiler guarantees the location-insensitivity of
its output. The assembler helps to make the code location-insensitive by
defining all labels as offsets relative to the beginning of the code vector
and causing instructions that refer to labels to index automatically off the
PDP-10 accumulator symbolically named M. M, like R, is set up by the
UUO handler, but it points to the code vector instead of the reference
vector. The code vector of an RSUBR can be frozen (using the FREEZE
SUBR) to prevent it from moving during debugging by DDT in the superior
operating-system process.
If the first element of an RSUBR is of TYPE PCODE ("pure code"), the
code vector of the RSUBR is pure and sharable. TYPE PCODE is of
PRIMTYPE WORD. The left half of the word specifies an offset into an
internal table of pure RSUBRs, and the right half specifies an offset into
the block of code where this RSUBR starts. The PCODE prints out as:
%<PCODE name:string offset:fix>
where name names the entry in the user's pure-RSUBR table, and offset
is the offset. (Obviously, PCODE is also the name of a SUBR, which
generates a pure code vector.) Pure pymdl does
not page-map: the block a RSUBRs may also move around, but
only by being included in MDL's page map at different places.PCODE names is read from the pure-code database
the first time an RSUBR in it is applied and cached for the session
(33.13). Once again M can be used exactly as before to do
location-independent address referencing. Individual pure code vectors
can be "unmapped" (marked as being not in primary storage but in their
original pure-code disk files) if the space in storage allocated for pure
code is exhausted. An unmapped There is nothing to unmap; a RSUBR is mapped in again whenever needed.
All pure RSUBRs are unmapped before a SAVE file is written, so that the
code is not duplicated on disk.SAVE
carries the PCODE word, which is all the RSUBR ever held. A purified
RSUBR must use RGLOC ("relative GLOC") instead of GLOC. RGLOC
produces objects of TYPE LOCR instead of LOCD.
<PCODE "WOFCH" 39> ⇒ %<PCODE "WOFCH" 39>
<TYPE <PCODE "WOFCH" 39>> ⇒ PCODE
<TYPEPRIM PCODE> ⇒ WORD
<TYPE <RGLOC FOO T>> ⇒ LOCR
<FREEZE [1 2]> ⇒ [1 2]
FREEZE copies its argument -- the copy is not ==? to the original -- and
returns it CHTYPEd to its PRIMTYPE, as chapter 24 describes; nothing
here relocates, so the copy is the whole of its effect.
19.5 TYPE-C and TYPE-W
In order to handle user NEWTYPEs reasonably, the internal TYPE codes for
them have to be able to be different from one MDL run to another.
Therefore, references to the TYPE codes must be in the reference vector
rather than the code vector. To help handle this problem, two TYPEs
exist, TYPE-C ("type code") and TYPE-W ("type word"), both of PRIMTYPE
WORD. They print as:
%<TYPE-C type primtype:atom>
%<TYPE-W type primtype:atom>
The SUBR TYPE-C produces an internal TYPE code for the type, and
TYPE-W produces a prototype "TYPE word" (appendix H) for an object of
that TYPE. The primtype argument is optional, included only as a check
against the call to NEWTYPE. TYPE-W can also take a third argument, of
PRIMTYPE WORD, whose right half is included in the generated "TYPE
word". If type is not a valid TYPE, a NEWTYPE is automatically done.
<TYPE-C FIX> ⇒ %<TYPE-C FIX WORD>
<TYPE-C LIST> ⇒ %<TYPE-C LIST LIST>
<TYPE-W LIST> ⇒ %<TYPE-W LIST LIST>
The codes themselves are the era's: FIX is 1, LIST is 11 (octal), the
values a compiled RSUBR tests with CAIE against the left half of a type
word. Appendix B lists every type with the code the bridge uses for it,
and a NEWTYPE made in a session gets the next free code as it did on the
era.
To be complete, a similar SUBR and TYPE should be mentioned here.
<PRIMTYPE-C type>
produces an internal "storage allocation code" (appendix H) for the type.
The value is of TYPE PRIMTYPE-C, PRIMTYPE WORD. In almost all cases
the SUBR TYPEPRIM gives just as much information, except in the case of
TEMPLATEs: all TYPEs of TEMPLATEs have the same TYPEPRIM, but they
all have different PRIMTYPE-Cs.
<PRIMTYPE-C LIST> ⇒ %<PRIMTYPE-C LIST>
19.6 RSUBR (the SUBR)
<RSUBR [code name decl ref ref ...]>
CHTYPEs its argument to an RSUBR, after checking it for legality.
RSUBR is rarely called other than in the MDL Assembler (Lebling, 1979).
It can be used if changes must be made to an RSUBR that are prohibited by
MDL's built-in safety mechanisms. For example, if the GVAL of name is an
RSUBR:
<SET FIXIT <CHTYPE ,name VECTOR>>$
[...]
...(changes to .FIXIT)...
<SETG name <RSUBR .FIXIT>>$
#RSUBR [...]
The legality check is the one 19.2 implies with the correction of the box
above: a VECTOR of at least two elements.
19.7 RSUBR-ENTRY
RSUBRs can have multiple entry points. An RSUBR-ENTRY can be applied to
arguments exactly like an RSUBR.
<RSUBR-ENTRY [rsubr-or-atom name:atom decl] offset:fix>
returns the VECTOR argument CHTYPEd to an RSUBR-ENTRY into the rsubr
at the specified offset. If the RSUBR-ENTRY is to have a DECL (RSUBR
style), it should come as shown.
<ENTRY-LOC rsubr-entry>
("entry location") returns the offset into the RSUBR of this entry.
<SET E <RSUBR-ENTRY [FOO BAR] 3>> ⇒ #RSUBR-ENTRY [FOO BAR]
<ENTRY-LOC .E> ⇒ 3
When the first element is an ATOM -- a forward reference to an RSUBR not
yet loaded, which is how the era's FBINs define every function after the
first -- pymdl resolves it through the GVAL at the moment of application,
and the entry runs in its parent's reference vector at its own offset.
Found along the way. The era's compiled packages define ONE purified
RSUBRper package and every other function as anRSUBR-ENTRYinto it:cacs.fbin'sREACSis%<PCODE "3CACS" 0>and the rest of CACS enters the same block at their offsets. SoENTRY-LOCis not a curiosity; it is how most of the era's compiled code is reached.
19.8 RSUBRs in files
There are three kinds of files that can contain RSUBRs, identified by
second names BINARY, NBIN and FBIN. There is nothing magic about these
names, but they are used by convention.
A BINARY file is a completely ASCII file containing complete impure
RSUBRs in character representation. Even a code vector appears as #CODE
followed by a UVECTOR of PRIMTYPE WORDs. BINARY files are generally
slow to load, because of all the parsing that must be done.
An NBIN file contains a mixture of ASCII characters and binary code. The
start of a binary portion is signalled to READ by the character control-C,
so naive readers of an NBIN file under ITS may incorrectly assume that it
ends before any binary code appears. An NBIN file cannot be edited with a
text editor. An RSUBR is written in NBIN format by being PRINTed on a
"PRINTB" CHANNEL. The RSUBRs in NBIN files are not purified either.
An FBIN file is actually part of a triad of files. The FBIN file(s)
itself is the impure part of a collection of purified RSUBRs. It is
simply ASCII and can be edited at will. (Exception: in the ITS and Tops-20
versions, the first object in the file should not be removed or changed in
any way, lest a "grim reaper" program for FBIN files think that the other
files in the triad are obsolete and delete them.) The pure code itself
resides (in the ITS and Tops-20 versions) in a special large file that
contains all currently-used pure code, or (in the Tenex version) in a file
in a special disk directory with first name the same as the name argument
to PCODE for the RSUBR. The pure-code file is page-mapped directly
into MDL storage in read-only mode. It can be unmapped when the pure storage
must be reclaimed, and it can be mapped at a different storage address when
pure storage must be compacted. pymdl reads the block out of that file
when it is first needed (33.13). There is also a "fixup" file (see below)
or portion of a file associated with the FBIN to round out the triad.
An initial MDL can have pure RSUBRs in it that were "loaded" during the
initialization procedure. The files are not page-mapped in until they are
actually needed. The "loading" has other side effects, such as the creation
of OBLISTs (chapter 18). Exactly what is pre-loaded is outside the scope
of this document.
pymdl reads all three kinds. BINARY and FBIN contain textual MDL;
NBIN mixes text with binary words. Archived ITS files also have a host
encoding that must be decoded. Sections 33.11 and 33.12 distinguish the
file contents from that encoding.
19.9 Fixups
The purpose of "fixups" is to correct references in the RSUBR to parts of
the interpreter that change from one release of MDL to the next. The reason
the fixups contain a release number is so that they can be completely
ignored when an RSUBR is loaded into the same release of MDL as that from
which it was last written out.
There are three forms of fixups, corresponding to the three kinds of RSUBR
files. ASCII RSUBRs, found in BINARY files, have ASCII fixups. The
fixups are contained in a LIST that has the following format:
(MDL-release:fix
name:atom value:fix (use:fix use:fix ...)
name:atom value:fix (use:fix use:fix ...)
...)
The fixups in NBIN files and the fixup files associated with FBIN files
are in a fast internal format that looks like a UVECTOR of PRIMTYPE
WORDs.
Fixups are usually discarded after they are used during the loading
procedure. However, if, while reading a BINARY or NBIN file the ATOM
KEEP-FIXUPS!- has a non-FALSE LVAL, the fixups will be kept, via an
association between the RSUBR and the ATOM RSUBR. It should be noted
that, besides correcting the code, the fixups themselves are corrected when
KEEP-FIXUPS is bound and true. Also, the assembler and compiler make the
same association when they first create an RSUBR, so that it can be
written out with its fixups.
In the case of pure RSUBRs (FBIN files), things are a little different.
If a pure-code file exists for this release of MDL, it is used immediately,
and the fixups are completely ignored. If a pure-code file for this release
doesn't exist, the fixup file is used to create a new copy of the file from
an old one, and also a new version of the fixup file is created to go with
the new pure-code file. This all goes on automatically behind the user's
back.
Found along the way. The "fast internal format" is not described anywhere in the manuals. Read off the era's object files and validated against the release-55 symbol table, a fixup block is: the release word, then per interpreter symbol a
SQUOZEname word (with its flag bits), a wordvalue,,first-location, and further locations packed two to a word, zero-terminated. Locations are 1-based offsets into the code vector. pymdl applies the block on every load, exactly as the era loader did -- for each symbol the running interpreter knows, the delta between its address and the dumped one is added to the right half of every listed word -- and leaves an unknown symbol's sites as dumped. That mechanism is how a binary compiled for one MUDDLE ran on the next, and it is whytests/data/eracan holdNBINs written by release 54 that run here as release 55 code (pdp10.apply_fixups).
33.10 How a compiled call site is reached
This and the sections after it are pymdl's; the manual stops at 19.9.
An RSUBR or RSUBR-ENTRY is an APPLICABLE, and EVAL applies it the way
it applies a SUBR, with the arguments evaluated. What happens then is
decided by the code slot, in this order:
- A pymac peer. If the name in the reference vector's second slot has a Python peer registered for this session (33.14), the peer is called with the arguments and the machine is never entered.
- An
ATOMin the code slot. TheRSUBR-ENTRYnames its parent byATOM; the parent'sGVALis fetched now, and the entry runs atENTRY-LOCin the parent's reference vector. CODE. The words are executed on the PDP-10 emulator withMpointing at them,Rat the reference vector and the arguments marshalled onto the MDL stacks (chapter 36).PCODE. The block is fetched from the pure-code database and executed from thePCODE's offset,Mpointing at the block's base.- Anything else in the slot is applied as itself -- the case of an
RSUBRwhose code is an interpretedFUNCTION, which is what the native compiler'sCOMPILEleaves when it refuses a function (chapter 34).
Steps 3 and 4 are the only doors into the emulator, and with
PYMDL_NO_PDP10 set both refuse with an error that names the door. A
session with every compiled routine peered runs entirely without them.
33.11 The NBIN format, as read
Nothing in the manuals gives the layout of an NBIN's binary portion beyond
"signalled by control-C". Read off the era object files:
^C (pad to a word boundary)
N ; code length, in words
N words of PDP-10 code
[0 name decl refs ...] ; the RSUBR VECTOR tail, as TEXT --
; element 1 is a placeholder for the code
(pad) L ; fixup block length, counting the release word
release ; 55, say
L-1 fixup words ; 19.9's fast format
The reader assembles the RSUBR from the parts: a #CODE UVECTOR of the
N words goes into slot 1 of the textual vector, the fixups are applied, the
vector is CHTYPEd to RSUBR, and if KEEP-FIXUPS is bound and true the
fixup UVECTOR is associated with the RSUBR under the indicator RSUBR,
as 19.9 says. The channel that serves an NBIN renders each 36-bit word as
its five 7-bit characters, so the reader's character position is the era's
character position and word i is at character 5i; that is what makes the
control-C's "pad to a word boundary" computable from the text side.
33.12 The bytes on disk
The era's binary files reach us through itstar, which unloaded the DUMP
tapes into Alan Bawden's evacuated file format: each 36-bit word is five
7-bit ASCII bytes when bit 0 is clear and the word is text, else a five-byte
escape whose lead byte, in the range 0o360 to 0o377, carries bits 35 to 32
and whose four bytes carry the rest. src/pymdl/its36.py decodes it,
transcribed from the format note in itstar's pack.c (1992). Because text
words decode to themselves, an ITS source file in the archive is readable as
it stands; because the escapes begin above 0o360, an object file is
recognisable from its first byte. pymdl uses exactly that to decide, by
CONTENT rather than by extension, whether an FLOADed file is binary: a
file named .NBIN that begins with < loads as source, which is how the era
compiler is built from MDL when its binaries are set aside (chapter 34).
33.13 The pure-code database
The "special large file that contains all currently-used pure code" of 19.8
is MUDSAV;SAV FILE on the tapes, and its companion is MUDSAV;FIXUP FILE.
The environment manual's chapter 4 describes the librarian's side of them;
pymdl reads them directly. A PCODE's name is looked up in the SAV file's
directory -- a header of directory-page numbers, each directory page a
count and then sorted pairs of a SIXBIT name and an info word giving the
block's length and start in 1024-word units -- and the block is the
GROUP-GLUEd concatenation of the package's code: every RSUBR of the
package in one run of words, each entry at the offset its PCODE names.
Found along the way. Disassembling the block named
WOFCH(chapter 35) showed the glue's shape: each routine's code, then its literals, then the assembler's global-symbol record -- the routine'sSQUOZEname with flag 1, its address, a count -- and the next routine. Two of that block's three entries are what the compiler'swofch.fbinandpopwr2.fbinpoint at; the third,SNAMES, an ITS.CALLroutine, no file on any tape names. The pure-code database can hold code nothing refers to.
The fixup file's record for a block is applied when the block is fetched, as
19.9 prescribes for the case where the pure-code file was made for another
release; for a block dumped for release 55 every delta is zero and the pass
changes nothing. <SAV-BLOCK name spec> answers a block as a #CODE
UVECTOR, which is what the debugger's UNASSM disassembles.
33.14 This machine's code
Every piece of machine code the tree carries has a Python function that
answers the same way, measured against it. The interpreter keeps a table
from RSUBR NAME to peer, and consults it at step 1 of 33.10 -- at the apply
boundary, by name, rather than by rebinding the GVAL. The reason is
19.3's own: an FBIN's reference vectors capture the RSUBR OBJECTS of the
routines they call when they are read, so a compiled call site reaches its
callee without ever consulting the GVAL, and a peer bound only there would
be bypassed by every compiled caller.
The era compiler's load registers peers for its helpers -- PRIM-CODE,
CONVERT-SAT, WOFCH, POPWR2, ATOSQ, HACK-NAME, PUREQ,
CONNECT-DIR -- so that the compiler compiles with the emulator switched
off; the library's assembly entries (DB-BUF-INIT, MAKE-STRING,
HASH-NAME and the rest) are bound to their natives when the library
loads, since their callers are interpreted MDL that goes through the
GVAL. Chapter 30 lists the library's, chapter 34 the compiler's, and
chapter 36 says what the emulator is left with once all of them are in
place: era artifacts that are machine code and nothing else.
Measured. The default compiler build had, until 2026-09-10, loaded
pureq.nbin'sRSUBRover the nativePUREQ, and on the emulator that code answeredTfor EVERYTHING -- a fresh list, a string, the FIX 5 -- because marshalled objects sit above the boundary its address compare uses. So the assembler's constant folding, which asks<NOT <PUREQ .ELE>>before rewriting an instruction, had never run here. With the native answering, it ran, and exposed thatRESTof aFORMdid not share theFORM's cells (chapter 10). A peer is not a shortcut; it is the answer the era machine would have given, and the emulator is not always that.