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 fileadd1.mudthat 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> ⇒ 2The file is the seven-line
RSUBRof 7.1.7's examples --TITLE,DECLARE,MOVE B* 1 (AB),ADDI B* 1,MOVSI A* <TYPE-CODE FIX>,JRST FINIS,END-- andFILE-ASSEMBLEanswers theLISTof names it assembled, printsTITLE: ADD1andDONE IN ... SECONDSon the messages channel, and writes anNBIN(33.11) a fresh interpreterFLOADs. The code vector is four instructions, the assembler's global-symbol record (theSQUOZEof the name, its address, a count) and nothing else; theDECLAREbecomes the third element of theRSUBR. 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 machineCODEhas nothing to run it (chapter 36).Found along the way.
MESSAGE-CHANNELcomes up in this revision of the assembler as theSTRING"TTY:", andASSEMBLE's own declaration of it is<OR CHANNEL FALSE>. The era ran the assembler compiled, where declarations are not checked; interpreted, a plainFILE-ASSEMBLEwith the default diedTYPE-MISMATCHbefore assembling anything. The era's own documentation of the package (mudman/assem.info) says what the initial value is meant to be -- "what.OUTCHANwas when loaded" -- and pymdl sets it to that as the package loads;FILE-ASSEMBLEitself 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
OPOBLISTis the one the manual names, and the table is the era's:mudbug/op.mud(MIT'smuddle/op.ubd047), every PDP-10 mnemonic as anOPCODEword, the accumulators in both fields, and the pseudo-ops. The oblist's full name isOP!-PACKAGE, because the package system made it (<MOBLIST OP!-PACKAGE>is how the assembler asks for it); in a session that has notUSEd anything the trailer!-OPalone names a fresh oblist, which is MDL 55's behaviour too (chapter 18). TheOPCODETYPEitself lives there, henceOPCODE!-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 theMOVEinstruction (similar to MIDAS).- A
VECTORindicates a constant. TheVECTORmay contain any number ofFORMs 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
FORMis simplyEVALed and the value returned is used.
Found along the way. "If the
ATOMhas 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.ucr005uses the labelFOO, which is also an interpreter symbol in the compiler image's table of 4633 names, andatosqgets away with it because itsFOOis a backward reference, already defined when met.popwr2.mud, written in the same style, put<JFFO A* FOO>ahead ofFOO's definition, the assembler resolved it to the interpreter'sFOOat once, and every compile that reachedPOPWR2died on a zero word there. Its forward label isGOTBIT. The same rule is why an assignedGVALoutranks a file's own forward-referenced label: a stand-inDSKDATEregistered as a global once stopped a 1977TELLfrom 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
MUDDLEoblist's values are the release's own. MDL 55 shipped the export of its DDT symbol table asMUDSAV;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 asksSQUOTAfor every atom it cannot otherwise place;SQUOTAanswers the address for a symbol the table has andFALSEfor one it does not, and thatFALSEis load-bearing, because only it lets an opcode or a register fall through to itsOPvalue. So<JRST FINIS>assembles toJRST 703120with a fixup onFINIS, and the emulator keys its interpreter services on those same addresses (chapter 36): a jump to703120in era-linked code meansFINISbecause this table says so. Three symbols the assembler reads asGVALs off theMUDDLEoblist are set as it loads, from the same table:$TLOSE(the type-table base,700000),NUMPRI(82, the number of primitive types) andGLOTOP(890, the top of the global vector). A site's ownMUDSAV;SQUOZE TAB55under 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
ASSEMBLEmust have been read with this oblist path in effect.<ASSEMBLE '(<TITLE TWO> <MOVSI A* <TYPE-CODE FIX>> ...)>typed at the listener assembles nothing and reportsUNDEFINED SYMBOL MOVSI,A*,TYPE-CODE: the reader interned those atoms onINITIALwhen the quote was read, and they are not theOPatoms the assembler knows. ACHANNELbody 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 isQUOTEd (notEVALed)."OPTIONAL"The rest of the arguments are optional (theRSUBRmust 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 theFORMgenerating the call (see"CALL"forFUNCTIONs in [3])."ARGS"This must be the lastSTRING. It says treat the rest of the arguments in theFORMas aLISTand pass it as the argument (see"ARGS"forFUNCTIONs in [3])."TUPLE"EVALthe rest of the arguments and pass them.
pymdl's own. These markers are honoured by the caller too. When the interpreter applies an
RSUBRit reads theDECLin the third slot exactly as it reads aFUNCTION's argument list: a"QUOTE"d parameter receives theFORM,"ARGS"the remaining forms as oneLIST,"TUPLE"the rest evaluated. That is MDL 55's behaviour, measured: its compiled<DEFINE QU1 ('A) <TYPE .A>>answersFORM, and an"ARGS"routine is entered at its one-argument entry with theLIST(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.mudandnlib.mudare MDL for most of their length and then<TITLE ...>sections --ADR,SYM-TO-DDT,DB-BUF-INITand their kin are assembly -- and on the era they were neverFLOADed: they went throughFILE-ASSEMBLE, whichEVALs the MDL statements as it goes and assembles the rest. A top-level<TITLE ...>underFLOADdiesUNBOUND-VARIABLE NAMEDon MDL 55, measured, and here. So a file with a top-levelTITLEis 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 thatVECTORis theTYPEwhose code is k,LOSEfirst (code 0),FIX(1),FLOAT(2),CHARACTER(3), and the codes noTYPEis named for -- 4, 8, 18 and eighteen more belowNUMPRI-- hold theATOMINTERNAL. The 83 built-ins end atOFFSET, code 82; the image'sNEWTYPEs follow in the order they were made. pymdl'sALLTYPEShad been in an order of its own, which nothing noticed until the unassembler (7.3) named everyTYPE-CODEone 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 intests/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-CODEof aNEWTYPE, whose code is aboveNUMPRI, is not a constant the assembler can write into an instruction; the assemblerPQUOTEs the type instead, andTYPE-WORDof one builds the type word in the reference vector. Both are why a type word for a userTYPEis 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.
SQUOZEis 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 theOPoblist (the three-argument form the compiler calls,<SQUOZE name 0 T>); and the library has theSQUOZEpackage, MIT'smprog/squoze.undr04, aFUNCTION, which is what<USE "SQUOZE">gives a program. The three agree.SQUOTAis the inverse over the release symbol table, squoze in and address out, andMUDOBJ(7.3) is the same table the other way.Found along the way. The assembler moves the atoms
*INSERT,ARG,SQUOZEandENDfrom the oblist it finds them on toOP, by<INSERT <REMOVE .X> ...>, and then defines its ownSQUOZEthere. On the era those were fresh atoms; here the first was pymdl's interpreterSQUOZE, which the move took offROOT. It is why theSQUOZEpackage must be a real package with anENTRYand not a shell over the builtin:UNASSMUSEs it afterCODINGhas loaded, and with a shell it found nothing, left$TLOSEunset, 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(theDPBtwin ofGETYP),CQUOTE(a reference-vector constant),SYMDEFandSYMDEF?(define and test an assembly-time symbol),INTGO(the interrupt check the compiler plants at the head of a routine:SKIPGE INTFLG/JSR LCKINT),MCALLandACALL(the calling UUOs of 33.3 with their argument counts),DSAVAC,IFOPSYSandGOPSYS(assemble differently for ITS and Tenex), andCONV-AC. The compiler's ownnewop.muddeclares which of these it expects "defined in the assembler (CODING)", and eighteen of its modules make 147 references to them by the!-OPtrailer without a<USE "OP">; read throughPACKAGE, each such module interns its ownOPatom 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 toUNDEFINED SYMBOL GETYP USED AT 5and 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.ucr005is 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'sMUDOBJandOUTPUT-MUDREF(muddle/mudhck.ubtb01) have the same shape.GLUEitself 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 aSTRINGtoSIXBITfor a.CALL, which nothing names and nothing loads; it is not reconstructed.JFFO,IMULby 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 --WOFCHon all 128 characters,POPWR2on 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.mudis a thin client of its superior: itVALRETs DDT command strings --addr$Bfor a breakpoint,addr$0Bto clear it,addr/SYM::to define a label -- and inserts symbols pair by pair withSYM-TO-DDT. pymdl plays the superior.ddt.pyowns the symbol table and the breakpoint set the emulator consults, parses exactly the command shapesrdb.mudemits, 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 asFINIS,MPOPJ,STACKF+3from the first stop.Measured (
tests/test_rdb.py), on aTOYassembled with a symbol table (ASSEMBLE ... T):<RBREAK TOY>answers"BROKEN"and sets one breakpoint atTOY's address, which the symbolsTOYand its first labelSTARTboth name andLASTnames two words on; running<TOY>then stops with$B TOY>>,TOY/examines205040000001(MOVSI A,TFIX),^Nsteps toTOY+1/ 201100000052, and$Pproceeds 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. Therdb.mudin the tree is MIT'smudbug/rdb.ur&t39; its ownTITLEsections 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.MUDOBJgives it the name of an interpreter address: the era'sMUDOBJ(muddle/mudhck.ubtb01) wasATOSQ's own internal entry, address in andSQUOZEword out orFALSE, and pymdl's answers from the release symbol table read backwards, soJRST 703120comes out asFINIS. The unassemblerCHTYPEs that word toMUDREFand prints it, and theMUDREFprinter is the era'sOUTPUT-MUDREFshape from the same file: a bar, the name decoded radix 40, a space --|FINIS-- which the|reader takes straight back to aMUDREF. 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 forADD1, printed as data.Found along the way. All three of those were wrong on 2026-09-10, and one small listing showed it.
MUDOBJansweredFALSEfor 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; theSQUOZEpackage was a shell, so afterCODINGhad moved the interpreter'sSQUOZEontoOPthe unassembler's$TLOSEwas never set and any type word stopped it; andALLTYPESnamedFIX's codeFLOAT. The listing above is the corrected one, andtests/test_assembler_listener.pykeeps 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.