The pymdl book

35. The assembler

The MDL assembler is an MDL program, and pymdl runs the era's own: the CODING package, byte for byte, interpreted. It turns a file of FORMs into RSUBRs whose code vectors are PDP-10 words, and those words are what the emulator of chapter 36 executes, or what the Python peers of chapter 33 stand in for when the machine is off. The manual's chapter 7 follows in full, with pymdl's notes; then the parts the manual could not have: what pymdl supplies the assembler with (the opcode table, the interpreter's symbols, the pseudo-op oblist), the sources in this tree that survive only as assembly and how they were read back, and the two tools of 7.2 and 7.3 as they run here.

Chapter 7. The Assembler

It is occasionally necessary to write MDL routines in assembly language, usually to interface with a feature of the operating system not available in the interpreter. The MDL assembler (which is also used by the MDL compiler) provides this ability.

7.1. The Assembler

The MDL assembler provides the MDL user with a means of writing RSUBRs directly in machine language. The assembler is also used as the object language of the compiler. This section is a description of the assembler, its use, and some of its pseudo-operations.

7.1.1. General Organization

The MDL assembler is written in MDL to produce code that runs in the MDL environment. It takes arguments in the following form:

<FILE-ASSEMBLE input-file:string
               output-file:string
               quick:boolean>

The arguments are an input-file containing MDL assembly code (possibly for several RSUBRs), an optional output-file in which to put the binary output (by default the same file as input but with second file name "NBIN"), and an optional third argument which tells whether to use NBIN format output, and which under normal circumstances should always be T. There are four other optional arguments which are the same as the second through fifth arguments of ASSEMBLE.

<ASSEMBLE body
          locals
          messages
          list
          symbols>

(All the arguments are optional with the exception of body.)

body may be a CHANNEL, in which case all instructions in the file associated with the CHANNEL are assembled, or it may be a structured object, in which case all instructions in the object are assembled.

locals specifies the OBLIST to use for local symbol lookup when the body is a CHANNEL. The default is <1 .OBLIST> when the assembler is called.

messages is a CHANNEL to receive error messages, etc. It defaults to .MESSAGE-CHANNEL.

list is a CHANNEL to receive an assembly listing. If list is not supplied, no listing is generated. If list is a non-FALSE non-CHANNEL, and messages is a CHANNEL, then the messages CHANNEL will receive the address of each label. If list is a FALSE, then no listing is produced. The default is .LINE-CHANNEL (Initially LINE-CHANNEL is FALSE.)

symbols indicates if true that a DDT symbol table of all the labels for use with "RDB" (see section 7.2) will be generated. The default is .MAKE-SYM-TABLE (Initially MAKE-SYM-TABLE is FALSE.)

pymdl's own. <USE "CODING"> loads the assembler; it is one of the MUDBUG tools (chapter 29) and is loaded on demand by a bare <FILE-ASSEMBLE ...> as well. A first assembly, from the file add1.mud that the book ships beside its examples:

<USE "CODING">
<FILE-ASSEMBLE "add1.mud" "add1.nbin" T>   ⇒ (ADD1)
<TYPE ,ADD1>                               ⇒ RSUBR
<ADD1 41>                                  ⇒ 42
<TYPE <1 ,ADD1>>                           ⇒ CODE
<LENGTH <1 ,ADD1>>                         ⇒ 7
<3 ,ADD1>                                  ⇒ #DECL ("VALUE" FIX FIX)
<FLOAD "add1.nbin">                        ⇒ "DONE"
<ADD1 1>                                   ⇒ 2

The file is the seven-line RSUBR of 7.1.7's examples -- TITLE, DECLARE, MOVE B* 1 (AB), ADDI B* 1, MOVSI A* <TYPE-CODE FIX>, JRST FINIS, END -- and FILE-ASSEMBLE answers the LIST of names it assembled, prints TITLE: ADD1 and DONE IN ... SECONDS on the messages channel, and writes an NBIN (33.11) a fresh interpreter FLOADs. The code vector is four instructions, the assembler's global-symbol record (the SQUOZE of the name, its address, a count) and nothing else; the DECLARE becomes the third element of the RSUBR. The result runs on the emulator. With the emulator off (PYMDL_NO_PDP10) the assembly still succeeds -- it is MDL -- and it is the call that refuses, NoEmulator, because a code slot of machine CODE has nothing to run it (chapter 36).

Found along the way. MESSAGE-CHANNEL comes up in this revision of the assembler as the STRING "TTY:", and ASSEMBLE's own declaration of it is <OR CHANNEL FALSE>. The era ran the assembler compiled, where declarations are not checked; interpreted, a plain FILE-ASSEMBLE with the default died TYPE-MISMATCH before assembling anything. The era's own documentation of the package (mudman/assem.info) says what the initial value is meant to be -- "what .OUTCHAN was when loaded" -- and pymdl sets it to that as the package loads; FILE-ASSEMBLE itself converts a "TTY:" passed as an argument to ,OUTCHAN, with the comment "CRETIN TOPS-20 MUDDLE". <SET MESSAGE-CHANNEL <>> silences the assembler, which is what the compiler build does.

7.1.2. The Assembler as a Program

The assembler also exists as a program called ASSEM, which encapsulates FILE-ASSEMBLE.

7.1.3. Format of Assembler's Source

The MDL assembler's equivalent of a line of code is a FORM. It assembles FORMs into instructions in much the same way that a typical assembler treats lines of source code. ATOMs at the top level (i.e. not in FORMs) are treated as labels. The FORMs are assembled based on the TYPE of the GVAL of the first ATOM in the FORM. The GVALs of ATOMs whose PNAMEs are the PDP-10 instructions are of TYPE OPCODE (PRIMTYPE WORD); the 'value word' has the 36 bit value of the instruction. For example, in

<MOVE A* 1 (B)>

the value of MOVE (in the OP OBLIST) is #OPCODE *200000000000*. This FORM is assembled directly into an instruction.

If the GVAL of the first ATOM in a FORM is something applicable (SUBR, FUNCTION, RSUBR etc.) the FORM is EVALed and the resulting SPLICE of FORMs is assembled. This is how macros and pseudo-ops are implemented. Notice that a pseudo-op or macro may produce no code by returning an empty SPLICE.

<USE "CODING">
<TYPE ,MOVE!-OP!-PACKAGE>       ⇒ OPCODE!-OP
,MOVE!-OP!-PACKAGE              ⇒ #OPCODE *200000000000*
,A*!-OP!-PACKAGE                ⇒ #OPCODE *000040000000*
<GASSIGNED? SIXBIT!-OP!-PACKAGE>    ⇒ T

pymdl's own. The OP OBLIST is the one the manual names, and the table is the era's: mudbug/op.mud (MIT's muddle/op.ubd047), every PDP-10 mnemonic as an OPCODE word, the accumulators in both fields, and the pseudo-ops. The oblist's full name is OP!-PACKAGE, because the package system made it (<MOBLIST OP!-PACKAGE> is how the assembler asks for it); in a session that has not USEd anything the trailer !-OP alone names a fresh oblist, which is MDL 55's behaviour too (chapter 18). The OPCODE TYPE itself lives there, hence OPCODE!-OP.

7.1.4. Instruction Assembly

Having determined that a FORM is going to assemble into an instruction, the assembler basically adds up the values of all the items in the FORM. In the case of items of TYPE OPCODE, a full 36 bit add is performed. Items of TYPE ADDRESS refer to labels in the program. Since the code is all location insensitive and will move around during garbage collection, references to labels must be indexed by accumulator M, the base register. Therefore, label symbols include an M in the left half and must also be added in with a full-word add. Items of PRIMTYPE WORD other than OPCODEs and ADDRESSes are ANDBed with *777777* before being added, and the carry from right half to left half is suppressed. When ATOMs are found in FORMs that are being assembled into instructions, special lookup rules are in effect. If the ATOM has a global value, that value is used. If the ATOM does not have a global value but has a local value, it is used. If the ATOM has neither a local nor global value, it is assumed to be a local symbol for this assembly. In this case the symbol value is used if it has already been defined, otherwise it is added to a list of as yet undefined symbols.

Objects other than ATOMs or PRIMTYPE WORDs cause the assembler to take special action.

  • LISTs are used to indicate swapping left and right halves. For example <MOVE (1)> would put the 1 in the index field of the MOVE instruction (similar to MIDAS).
  • A VECTOR indicates a constant. The VECTOR may contain any number of FORMs to be assembled at the end of the program. For example: <PUSH TP* [<1 (1)>]> pushes a constant containing 1 in the right and left halves.
  • A FORM is simply EVALed and the value returned is used.

Found along the way. "If the ATOM has a global value, that value is used" is the rule that made one reconstruction wrong before it was right. A forward reference to a label not yet defined asks the interpreter's symbol table first: atosq.ucr005 uses the label FOO, which is also an interpreter symbol in the compiler image's table of 4633 names, and atosq gets away with it because its FOO is a backward reference, already defined when met. popwr2.mud, written in the same style, put <JFFO A* FOO> ahead of FOO's definition, the assembler resolved it to the interpreter's FOO at once, and every compile that reached POPWR2 died on a zero word there. Its forward label is GOTBIT. The same rule is why an assigned GVAL outranks a file's own forward-referenced label: a stand-in DSKDATE registered as a global once stopped a 1977 TELL from assembling its <INTERNAL-ENTRY IDSKDATE 0>, which is one reason the name is withheld from the interpreter (Appendix D).

7.1.5. Initial Symbols

The OBLIST structure in effect during assembly is

(op mud! DEFAULT local root)

The OBLIST op is named OP and contains the PDP-10 opcodes, the MDL accumulator definitions (in both accumulator and address fields), and the pseudo-ops. The OBLIST mud! is named MUDDLE and contains values of many labels in the interpreter. This enables programs to do things like <JRST FINIS>, the standard way to exit from an RSUBR. When an instruction is assembled using a symbol from the MUDDLE OBLIST, a fixup is also generated so that, if the symbol gets a different value in a new MDL, the code can be fixed up when it is loaded. local is the user's local symbol OBLIST and root is the ROOT OBLIST.

As stated earlier, every accumulator has two symbols associated with it, one for the address field and one for the accumulator field. This is because there is no syntax to specify which field is intended. The address symbol is simply the accumulator's name, and the accumulator symbol is the name with an asterisk (*) appended to it; e.g. A versus A*.

pymdl's own. The MUDDLE oblist's values are the release's own. MDL 55 shipped the export of its DDT symbol table as MUDSAV;SQUOZE TAB55, 1031 symbols, the same table that validated fixup records at load time (33.9), and pymdl carries it (mdl/mudsav/squoze.tab55.txt, the era's binary transcribed to octal and verified word-identical). The assembler asks SQUOTA for every atom it cannot otherwise place; SQUOTA answers the address for a symbol the table has and FALSE for one it does not, and that FALSE is load-bearing, because only it lets an opcode or a register fall through to its OP value. So <JRST FINIS> assembles to JRST 703120 with a fixup on FINIS, and the emulator keys its interpreter services on those same addresses (chapter 36): a jump to 703120 in era-linked code means FINIS because this table says so. Three symbols the assembler reads as GVALs off the MUDDLE oblist are set as it loads, from the same table: $TLOSE (the type-table base, 700000), NUMPRI (82, the number of primitive types) and GLOTOP (890, the top of the global vector). A site's own MUDSAV;SQUOZE TAB55 under the file root wins over the shipped one, as it should: a differently linked interpreter needs its table.

Found along the way. A structured body handed to ASSEMBLE must have been read with this oblist path in effect. <ASSEMBLE '(<TITLE TWO> <MOVSI A* <TYPE-CODE FIX>> ...)> typed at the listener assembles nothing and reports UNDEFINED SYMBOL MOVSI, A*, TYPE-CODE: the reader interned those atoms on INITIAL when the quote was read, and they are not the OP atoms the assembler knows. A CHANNEL body has no such problem, because the assembler reads it itself, and it is what the compiler hands over.

7.1.6. Macro Writing

Whenever an element or subelement of an instruction is a FORM and the first element of the FORM has an APPLICABLE GVAL, the FORM is evaluated and the result (unless it is a SPLICE) is re-evaluated as if it were in place of the FORM. This feature constitutes the assembler's macro facility.

For compatibility between 'top-level' macros, which generate whole instructions, and macros which generate parts of an instruction, top-level macros may wish to return several instructions. To indicate that what is returned is several instructions, it is necessary to return an object of type SPLICE (PRIMTYPE LIST). The elements of the SPLICE are treated as individual instructions. An empty SPLICE may be returned from a macro which is part of an instruction, and the effect is as if a 0 were returned. This is the only SPLICE which may be returned from a macro which is a part of an instruction.

7.1.7. Pseudo Operations

The next part of this document will describe pseudo-ops available in the MDL assembler. There is no difference between a pseudo-op and a macro in the assembler except that the pseudo-operations are supplied by the system.

<TITLE name:string>

This is about the only required pseudo-op. It must be the first instruction to be assembled. It takes one argument, the name of the RSUBR being assembled. If additional TITLEs are found in a file being assembled, they are assumed to both end the previous RSUBR and begin the next. The assembler prints each TITLE on the messages CHANNEL as it is encountered.

<SUB-ENTRY entry:atom decl>

This pseudo-op is used to define additional RSUBR-ENTRYs for the RSUBR being assembled. The entry argument is the name of the RSUBR-ENTRY and the optional decl argument is a DECL for the entry.

<INTERNAL-ENTRY entry:atom nargs:fix>

is used to create an INTERNAL-ENTRY for a GLUEable RSUBR. Its arguments are the name of the INTERNAL-ENTRY and the number of arguments that will have been pushed on the stack for it when it is called. See also section 7.1.9 for details on writing GLUEable RSUBRs.

<DECLARE ("VALUE" decl decl decl ....)>

is used to supply declarations for the RSUBR named in the TITLE. It must occur before any code-generating instructions. DECLARE takes a LIST as its one argument. The format of the LIST is as described in [3]. The string "VALUE" is optional; if supplied it causes the first decl to declare the TYPE of the value of the RSUBR. Each additional decl is associated with one argument. Special STRINGs may also appear in the LIST with the following meanings:

  • "QUOTE" The next argument is QUOTEd (not EVALed).
  • "OPTIONAL" The rest of the arguments are optional (the RSUBR must supply any defaults for these).
  • "CALL" If this appears, it must be directly after the "VALUE" decl. It says there is one argument and it is the FORM generating the call (see "CALL" for FUNCTIONs in [3]).
  • "ARGS" This must be the last STRING. It says treat the rest of the arguments in the FORM as a LIST and pass it as the argument (see "ARGS" for FUNCTIONs in [3]).
  • "TUPLE" EVAL the rest of the arguments and pass them.

pymdl's own. These markers are honoured by the caller too. When the interpreter applies an RSUBR it reads the DECL in the third slot exactly as it reads a FUNCTION's argument list: a "QUOTE"d parameter receives the FORM, "ARGS" the remaining forms as one LIST, "TUPLE" the rest evaluated. That is MDL 55's behaviour, measured: its compiled <DEFINE QU1 ('A) <TYPE .A>> answers FORM, and an "ARGS" routine is entered at its one-argument entry with the LIST (33.10).

<END>

indicates the end of an RSUBR or group of RSUBRs. Only the text between TITLE and END pseudo-ops will be processed by the assembler. This makes it possible to intermix assembler source code and normal MDL source code in the same file (although assembly must be done before compilation in such cases).

Found along the way. Two of the debugging tools pymdl carries end this way. rdb.mud and nlib.mud are MDL for most of their length and then <TITLE ...> sections -- ADR, SYM-TO-DDT, DB-BUF-INIT and their kin are assembly -- and on the era they were never FLOADed: they went through FILE-ASSEMBLE, which EVALs the MDL statements as it goes and assembles the rest. A top-level <TITLE ...> under FLOAD dies UNBOUND-VARIABLE NAMED on MDL 55, measured, and here. So a file with a top-level TITLE is a file for the assembler, and that is how the self-hosting harness of 34.9 loads such files.

<TYPE-CODE type:atom>

allows references to the internal TYPE codes for both system and user defined TYPEs. It takes one argument, the MDL TYPE name. For example:

<MOVSI A* <TYPE-CODE FIX>>

puts the TYPE code for FIX into the left half of accumulator A.

<TYPE-WORD type:atom any ...>

generates a reference to a word containing the TYPE code for type in the left half and possibly other junk in the right half. The first argument is the TYPE name and the rest of the arguments are optional but if supplied are added into the right half. If the TYPE is an initial TYPE and no right half is generated, a reference to the 'ST type' location in the interpreter is generated. For example,

<PUSH TP* <TYPE-WORD FIX>>
<PUSH TP* [0]>

would push a FIX 0 on the stack.

Measured, MDL 55, 2026-09-10. A type code is a position in <ALLTYPES>: element k+1 of that VECTOR is the TYPE whose code is k, LOSE first (code 0), FIX (1), FLOAT (2), CHARACTER (3), and the codes no TYPE is named for -- 4, 8, 18 and eighteen more below NUMPRI -- hold the ATOM INTERNAL. The 83 built-ins end at OFFSET, code 82; the image's NEWTYPEs follow in the order they were made. pymdl's ALLTYPES had been in an order of its own, which nothing noticed until the unassembler (7.3) named every TYPE-CODE one type off; it is MDL 55's vector now, and every named built-in's <CHTYPE <TYPE-C x> FIX> is its position less one (tests/test_assembler_listener.py, and the whole census in tests/test_type_codes_era.py).

<1 <ALLTYPES>>     ⇒ LOSE
<2 <ALLTYPES>>     ⇒ FIX
<5 <ALLTYPES>>     ⇒ INTERNAL
<57 <ALLTYPES>>    ⇒ WORD
<83 <ALLTYPES>>    ⇒ OFFSET
<CHTYPE <TYPE-C WORD> FIX>   ⇒ 56

TYPE-CODE of a NEWTYPE, whose code is above NUMPRI, is not a constant the assembler can write into an instruction; the assembler PQUOTEs the type instead, and TYPE-WORD of one builds the type word in the reference vector. Both are why a type word for a user TYPE is a reference-vector slot and not an immediate.

<GETYP ac type:atom>

has the same form as a PDP-10 instruction. It gets the TYPE code for type into the right half of its accumulator from its address. This is done by generating an appropriate LDB (load byte) instruction.

<MQUOTE object:any>

allows the RSUBR to reference garbage collected space. It adds its argument to the RVECTOR (if it isn't already there) and evaluates to an address of the form offset(R), pointing to the value word for object.

<PQUOTE object:any>

is identical to <<MQUOTE object:any> -1>, i.e. it points to the type-word, not the value-word. This is a more consistent way to look at things.

<IQUOTE object:any label:atom>

is like PQUOTE except that this will add a new element to the reference VECTOR each time called. The optional label if given defines the ATOM to be a label referring to that element. This is the only way to refer to that element again.

<PSEUDO arg:any>

evaluates its argument for its side effects and assembles no code.

<SIXBIT string>

makes SIXBIT of the legal characters of string.

<SQUOZE string sqbits:word>

makes SQUOZE of the legal characters of string and sticks the low-order four bits of the optional sqbits in the high-order four bits of the value. See the MIDAS Manual [4] for an explanation of the SQUOZE code.

<USE "SQUOZE">
<SQUOZE "FOO">                ⇒ #WORD *014544175000*
<SQUOZE "FOO" *17*>           ⇒ #WORD *754544175000*
<SQUOTA <SQUOZE "FINIS">>     ⇒ 230992
<==? <MUDOBJ 230992> <CHTYPE <SQUOZE "FINIS"> FIX>>   ⇒ T

pymdl's own. SQUOZE is radix 40 over the alphabet space, the digits, the letters, ., $, % -- six characters to a word, with four flag bits above -- and it exists three times over, as it did on the era. The interpreter has one; the assembler defines its own on the OP oblist (the three-argument form the compiler calls, <SQUOZE name 0 T>); and the library has the SQUOZE package, MIT's mprog/squoze.undr04, a FUNCTION, which is what <USE "SQUOZE"> gives a program. The three agree. SQUOTA is the inverse over the release symbol table, squoze in and address out, and MUDOBJ (7.3) is the same table the other way.

Found along the way. The assembler moves the atoms *INSERT, ARG, SQUOZE and END from the oblist it finds them on to OP, by <INSERT <REMOVE .X> ...>, and then defines its own SQUOZE there. On the era those were fresh atoms; here the first was pymdl's interpreter SQUOZE, which the move took off ROOT. It is why the SQUOZE package must be a real package with an ENTRY and not a shell over the builtin: UNASSM USEs it after CODING has loaded, and with a shell it found nothing, left $TLOSE unset, and could not unassemble a type word.

<BYTE bound:fix byte-size:fix location>

Example: <BYTE 1 36 (C)> is like <(*014300*) (C)>.

<ARG argnum:fix>

is like <(AB) <* 2 <- .argnum 1>>>. ARG should not be used in GLUEable code.

<STACK sym1:atom sym2:atom sym3:atom ...>

makes sym1 a symbol for <(TB) 0>, sym2 a symbol for <(TB) 2>, sym3 a symbol for <(TB) 4>, etc. STACK should not be used in GLUEable code.

<DPUSH ac args>
<DPOP ac args>
<DMOVE ac args>
<DMOVEM ac args>

are the double-word PDP-10 instructions. For example,

<DPUSH ac args>

expands into

#SPLICE (<PUSH ac args> <PUSH ac args 1>)

<UNDEF? symbol:atom>

evaluates to true only if the symbol has previously in the code been used as a symbol, but has not been defined.

<IF-NEEDED symbol:atom instructions ...>

If <UNDEF? symbol> evaluates to true, then all the instructions are inserted at the current location, otherwise they are not.

<*INSERT file-spec:string>

takes a file and reads instructions from it and inserts the instructions read at the current place.

Found along the way. The vendored assembler defines pseudo-ops the manual does not list, and the compiler's output uses several of them: PUTYP (the DPB twin of GETYP), CQUOTE (a reference-vector constant), SYMDEF and SYMDEF? (define and test an assembly-time symbol), INTGO (the interrupt check the compiler plants at the head of a routine: SKIPGE INTFLG / JSR LCKINT), MCALL and ACALL (the calling UUOs of 33.3 with their argument counts), DSAVAC, IFOPSYS and GOPSYS (assemble differently for ITS and Tenex), and CONV-AC. The compiler's own newop.mud declares which of these it expects "defined in the assembler (CODING)", and eighteen of its modules make 147 references to them by the !-OP trailer without a <USE "OP">; read through PACKAGE, each such module interns its own OP atom and the trailer forks a fresh oblist -- MDL 55 does the same, measured -- so the compiler build binds the forks to the one table (tools/era_compiler.py). A fork left unbound assembled to UNDEFINED SYMBOL GETYP USED AT 5 and a hole in the code.

7.1.8. The Type RSUBR

An RSUBR is a MDL object of PRIMTYPE VECTOR. The first element of an RSUBR is always of TYPE CODE (or PCODE). CODE is of PRIMTYPE UVECTOR, consisting of words or instructions. The second element of an RSUBR is an ATOM which is the RSUBR's name. If the RSUBR has declarations they are the third element. The rest of the RSUBR contains MDL objects which must be referenced by the code.

An RSUBR-ENTRY is a VECTOR of two or three items. The first item is either an RSUBR or an ATOM whose GVAL is an RSUBR, the second is an ATOM which is the entry's name and the third is a DECL for the entry. The difference between an RSUBR and an RSUBR-ENTRY is that an RSUBR always starts running at the beginning of the code when it is called while an RSUBR-ENTRY usually starts running somewhere in the middle of the code.

Chapter 33 has the rest: the reference vector, pure and impure code, the linking flag, the fixups, and the files.

7.1.9. Writing Gluable RSUBRs

Certain conventions must be followed when writing hand coded RSUBRs in order to get the most benefit from GLUEing. If the RSUBR (or RSUBR-ENTRY) has "TUPLE" in its DECL, it is already in the best shape possible. In all other cases, the code after the TITLE or SUB-ENTRY pseudo-operation should simply push the arguments onto the TP stack and PUSHJ P* to one of the internal entries based on the number of items on the stack. After the PUSHJ it should do a <JRST FINIS>. An internal entry is set up by using the INTERNAL-ENTRY pseudo-op which takes two arguments: an atom and a fix. The atom acts as if it were a label on the next instruction and may be used as a label. The fix specifies how many items (type-value pairs) are on the stack at this internal entry. In the simple case where there are no optional arguments, only one internal entry exists and its number argument is exactly the required number of arguments. If optional arguments exist, some kind of dispatch will have to be done.

In the rest of the body of the RSUBR, no references to AB or TB (through the ARG or STACK pseudo-ops or directly) can be made, because after GLUEing their contents may be meaningless. All references to the TP stack must be indexed by TP. The usual precautions concerning the possible movement of code if an INTGO or MCALL is done also apply (i.e. the use of <SUBM M* (P)> at the beginning and <JRST MPOPJ> at the end of the code are essentially mandatory).

pymdl's own. The surviving era assembly in this tree is written to exactly this recipe, and atosq.ucr005 is the smallest complete example: DPUSH TP* (AB), PUSHJ P* IATOSQ, JRST FINIS, then <INTERNAL-ENTRY IATOSQ 1>, SUBM M* (P), the body, JRST MPOPJ. The era's MUDOBJ and OUTPUT-MUDREF (muddle/mudhck.ubtb01) have the same shape. GLUE itself is chapter 31.

35.2 What survives only as assembly

Every piece of machine code in the tree has a source, or a record of why it has none, and compil/CHANGES.md keeps the ledger. Three routines of the compiler's are the interesting cases (33.13):

file what the era left what the file is
atosq.ucr005 the era's own assembler source, 448 bytes, and four shipped binaries vendored verbatim; assembles to the same 20 words as every one of the binaries
popwr2.mud only a <PCODE "WOFCH" 39> pointing into a pure-code block assembler source read back from the block's disassembly; 16 of 16 words identical, the literal -35 and the symbol record included
wofch.mud only a <PCODE "WOFCH" 0> read back the same way; the six code words identical, and the block's entry then ends in one zero word where every revision of this assembler records the TITLE name, so the block's WOFCH came from an assembler we do not have

Measured, 2026-09-09 (tests/test_assembler_reconstructions.py). The calibration is the first row: the era's own source through this interpreted assembler reproduces the era's own binary word for word, and that is what lets the other two rows mean something. The block itself (9006255/mudsav/sav.file, "WOFCH") holds a third routine at words 7-38, SNAMES, an ITS routine folding a STRING to SIXBIT for a .CALL, which nothing names and nothing loads; it is not reconstructed. JFFO, IMUL by a replication constant and byte-pointer loops are not things the compiler emits, so the block was hand-written and its source is lost. Every one of the three has a Python peer (33.14) that is held against the era words -- WOFCH on all 128 characters, POPWR2 on thirty inputs, zero included -- so the compiler builds and compiles with the emulator off.

ATOSQ has one more part to its story, because the assembler needs an ATOSQ to assemble anything: coding.mud FLOADs CLR;ATOSQ NBIN while it is itself loading. In the from-source build that FLOAD gets a bootstrap written in MDL, atosq.mud, and the assembled atosq.ucr005 replaces it on every atom the bootstrap was on once the assembler is up. primhk.mud, sbrnam.mud and connect-dir.mud are the era's own files too, byte for byte, but they are assembler and nothing FLOADs them; what runs is a Python peer or a staged binary. And assem.mud, also byte-identical to its original, is not assembler source of anything: it is assem.all, the MIDAS batch script that assembled the interpreter's modules.

7.2. Debugging Binary Code

Binary code produced by the MDL assembler or the MDL compiler may be debugged with DDT, like any other binary code. However, an interface between that code and the DDT environment must exist. That interface is the "RDB" PACKAGE. It is obtained by

<USE "RDB">

The symbol table optionally produced by the assembler can be passed to DDT and at the same time the RSUBR frozen (moved out of normal garbage-collected space) by:

<RFREEZE name-of-rsubr:atom>

Note that name-of-rsubr may also refer to an RSUBR-ENTRY.

<RBREAK name-of-rsubr:atom>

is similar, but in addition causes DDT to put a breakpoint at the first instruction of the RSUBR.

If there is no symbol table, RFREEZE and RBREAK merely freeze the RSUBR and pass up symbols for the RSUBR name and any sub-entries.

In all cases the symbols passed up are made up of the legal SQUOZE characters (letters, digits, $, %, .) of the name, up to six characters. For example the ATOM FOO-BLECH becomes the symbol FOOBLE.

<ADR object:any>

returns the address of object as a FIX. For example, <ADR rsubr> would return the location of the rsubr in core.

<RUNBREAK name-of-rsubr:atom>

clears the breakpoint(s) at the beginning of the RSUBR and of any of its sub-entries.

<USE "CODING">
<FILE-ASSEMBLE "add1.mud" "add1.nbin" T>   ⇒ (ADD1)
<USE "RDB">
<RFREEZE ADD1>                             ⇒ "FROZEN"
<RFREEZE ADD1>                             ⇒ "FROZEN"
<TYPE <ADR <1 ,ADD1>>>                     ⇒ FIX
<ADD1 41>                                  ⇒ 42

pymdl's own. On ITS, MDL ran as an inferior of DDT, and rdb.mud is a thin client of its superior: it VALRETs DDT command strings -- addr$B for a breakpoint, addr$0B to clear it, addr/SYM:: to define a label -- and inserts symbols pair by pair with SYM-TO-DDT. pymdl plays the superior. ddt.py owns the symbol table and the breakpoint set the emulator consults, parses exactly the command shapes rdb.mud emits, and serves the stop: a small examine, deposit, step and proceed loop over the live core, with addresses rendered through the symbols it has been taught. Its table starts as the era developer's did, with the release's full DDT symbol table -- globals and local labels, from MIT's own dumped image (mdl/mudsav/ddt.syms55) -- so interpreter locations read as FINIS, MPOPJ, STACKF+3 from the first stop.

Measured (tests/test_rdb.py), on a TOY assembled with a symbol table (ASSEMBLE ... T): <RBREAK TOY> answers "BROKEN" and sets one breakpoint at TOY's address, which the symbols TOY and its first label START both name and LAST names two words on; running <TOY> then stops with $B TOY>>, TOY/ examines 205040000001 (MOVSI A,TFIX), ^N steps to TOY+1/ 201100000052, and $P proceeds to the answer 42. <RUNBREAK TOY> answers "UNBROKEN" and the call runs straight through. A deposit at the stop patches the running machine code, which is what RDB exists for. The rdb.mud in the tree is MIT's mudbug/rdb.ur&t39; its own TITLE sections are the assembly of 7.1.7's box.

7.3. Unassembling Binary Code

Converting compiled or assembled binary code back into something resembling the original assembler source code is an operation that is performed primarily in one situation: tracking down a MDL compiler bug. It is, however, almost invaluable in that situation. The PACKAGE containing the unassembler is "UNASSM". The main entry is

<UNASSEMBLE code:rsubr-or-group
            output:channel-or-string
            glue?:boolean>

code is the object being unassembled. It is either an RSUBR (not an RSUBR-ENTRY, note), or an ATOM whose LVAL is a group (as created by GROUP-LOAD).

output is where to put the output. If it is a STRING, then the output is put in a file with that name. If output is a CHANNEL, then output is done on that CHANNEL. The file is "code UNASSM" by default.

glue? (by default T) tells whether there are glue bits for the code loaded. If there are none, this argument should be given as a FALSE.

The output produced by UNASSEMBLE is like the MDL compiler's assembler input, with the addition of comments which give code and stack offsets for stack slots referenced. This information is useful in tracing exactly what is going on in the code, but it is not always accurate, since the compiler's stack model is sometimes too complex for the unassembler to understand.

<USE "CODING">
<FILE-ASSEMBLE "add1.mud" "add1.nbin" T>   ⇒ (ADD1)
<USE "UNASSM">
<UNASSEMBLE ,ADD1 ,OUTCHAN <>>             ⇒ T

which prints

	<TITLE ADD1>

	<DECLARE ("VALUE" FIX FIX)>
	<MOVE	B* (AB) 1>
	<ADDI	B* 1 >
	<MOVSI	A* <TYPE-CODE FIX>>
	<JRST	|FINIS >
	<(*50525*) *14200*>
	<0>
	<2>

pymdl's own. The unassembler is MIT's mudbug/unassm.ubtb25, with its 512-entry instruction table, and three of the interpreter primitives it leans on are pymdl's. MUDOBJ gives it the name of an interpreter address: the era's MUDOBJ (muddle/mudhck.ubtb01) was ATOSQ's own internal entry, address in and SQUOZE word out or FALSE, and pymdl's answers from the release symbol table read backwards, so JRST 703120 comes out as FINIS. The unassembler CHTYPEs that word to MUDREF and prints it, and the MUDREF printer is the era's OUTPUT-MUDREF shape from the same file: a bar, the name decoded radix 40, a space -- |FINIS -- which the | reader takes straight back to a MUDREF. And a type code is named by indexing <ALLTYPES> by code, which is why that vector is in MDL 55's order (7.1.7's box). The last three words of the listing are the assembler's global-symbol record for ADD1, printed as data.

Found along the way. All three of those were wrong on 2026-09-10, and one small listing showed it. MUDOBJ answered FALSE for every address, on the reasoning that pymdl has no core for an address to point into -- but the symbol table is not core -- and the printer compared its word against a table nothing filled, so every interpreter reference was a bare octal number; the SQUOZE package was a shell, so after CODING had moved the interpreter's SQUOZE onto OP the unassembler's $TLOSE was never set and any type word stopped it; and ALLTYPES named FIX's code FLOAT. The listing above is the corrected one, and tests/test_assembler_listener.py keeps it so.

tools/era_oracle.py show CASE is the same idea across two machines: MDL 55's words for an oracle case beside ours, disassembled and aligned, which is how a one-word difference in 34.8 is read.