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 TYPE CODE or PCODE and is the impure or pure code vector respectively.
  • The second element is an ATOM and specifies the name of the RSUBR.
  • The third element is of TYPE DECL and declares the type/structure of the RSUBR'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 no DECL, and MDL 54 loads it: the 1977 TAA;TELLB NBIN holds RSUBRs of length 2 (GET-NAME), 3 (DSKDATE, GXUNAME) and 8 (TELL), measured under apsim 2026-08-29. So RSUBR (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 ATOM. In pymdl no call is ever permanently linked: the UUO handler resolves the 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-LINK keeps the flag and answers as the manual says, and nothing consults it: there is no faster path for a linked call to take. QUICK-RSUBR and QUICK-ENTRY exist as TYPEs, 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 RSUBRs may also move around, but only by being included in MDL's page map at different places. pymdl does not page-map: the block a 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 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. There is nothing to unmap; a 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 RSUBR per package and every other function as an RSUBR-ENTRY into it: cacs.fbin's REACS is %<PCODE "3CACS" 0> and the rest of CACS enters the same block at their offsets. So ENTRY-LOC is 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 SQUOZE name word (with its flag bits), a word value,,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 why tests/data/era can hold NBINs 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:

  1. 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.
  2. An ATOM in the code slot. The RSUBR-ENTRY names its parent by ATOM; the parent's GVAL is fetched now, and the entry runs at ENTRY-LOC in the parent's reference vector.
  3. CODE. The words are executed on the PDP-10 emulator with M pointing at them, R at the reference vector and the arguments marshalled onto the MDL stacks (chapter 36).
  4. PCODE. The block is fetched from the pure-code database and executed from the PCODE's offset, M pointing at the block's base.
  5. Anything else in the slot is applied as itself -- the case of an RSUBR whose code is an interpreted FUNCTION, which is what the native compiler's COMPILE leaves 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's SQUOZE name with flag 1, its address, a count -- and the next routine. Two of that block's three entries are what the compiler's wofch.fbin and popwr2.fbin point at; the third, SNAMES, an ITS .CALL routine, 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's RSUBR over the native PUREQ, and on the emulator that code answered T for 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 that REST of a FORM did not share the FORM'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.