The pymdl book
9. Data types
6.1. General
A MDL object consists of two parts: its TYPE and its "data part" (appendix
1). The interpretation of the "data part" of an object depends of course on
its TYPE. The structural organization of an object, that is, the way it is
organized in storage, is referred to as its "primitive type". While there are
many different TYPEs of objects in MDL, there are fewer primitive types.
All structured objects in MDL are ordered sequences of elements. As such,
there are SUBRs which operate on all of them uniformly, as ordered
sequences. On the other hand, the reason for having different primitive types
of structured objects is that there are useful qualities of structured objects
which are mutually incompatible. There are, therefore, SUBRs which do not
work on all structured objects: these SUBRs exist to take full advantage of
those mutually incompatible qualities. The most-commonly-used primitive types
of structured objects are discussed in chapter 10, along with those special
SUBRs operating on them.
It is very easy to make a new MDL object that differs from an old one only in
TYPE, as long as the primitive type is unchanged. It is relatively
difficult to make a new structured object that differs from an old one in
primitive type, even if it has the same elements.
Before talking any more about structured objects, some information needs to be
given about TYPEs in general.
pymdl's own. "Its
TYPEand its data part" is one 36-bit word and another on the era, and the type word's left half is a small integer -- the type code -- which chapter 35 shows the assembler writing into instructions. Here an object is a Python object carrying a type name, and the codes exist only where something needs them: the emulator, the marshaller, and the assembler'sTYPE-CODE. They are pinned to MDL 55's own census all the same, every built-inTYPE, because compiled code bakes them in (chapter 35 has the vector, Appendix B the table).
6.2. Printed Representation
There are many TYPEs for which MDL has no specific representation. There
aren't enough different kinds of brackets. The representation used for
TYPEs without any special representation is
#type representation-as-if-it-were-its-primitive-type
READ will understand that format for any TYPE, and PRINT will use it
by default. This representational format will be referred to below as "#
notation". It was used above to represent FUNCTIONs.
6.3. SUBRs Related to TYPEs
6.3.1. TYPE
<TYPE any>
returns an ATOM whose PNAME corresponds to the TYPE of any. There is
no TYPE "TYPE". To type a TYPE (aren't homonyms wonderful?), just type
the appropriate ATOM, like FIX or FLOAT or ATOM etc. However, in this
document we will use the convention that a metasyntactic variable can have
type for a "data type": for example, foo:type means that the TYPE of foo is
ATOM, but the ATOM must be something that the SUBR TYPE can return.
Examples:
<TYPE 1> ⇒ FIX
<TYPE 1.0> ⇒ FLOAT
<TYPE +> ⇒ ATOM
<TYPE ,+> ⇒ SUBR
<TYPE GEORGE> ⇒ ATOM
6.3.2. PRIMTYPE
<PRIMTYPE any>
evaluates to the primitive type of any. The PRIMTYPE of any is an ATOM
which also represents a TYPE. The way an object can be manipulated
depends solely upon its PRIMTYPE; the way it is evaluated depends upon
its TYPE.
Examples:
<PRIMTYPE 1> ⇒ WORD
<PRIMTYPE 1.0> ⇒ WORD
<PRIMTYPE ,+> ⇒ WORD
<PRIMTYPE GEORGE> ⇒ ATOM
6.3.3. TYPEPRIM
<TYPEPRIM type>
returns the PRIMTYPE of an object whose TYPE is type. type is, as usual,
an ATOM used to designate a TYPE.
Examples:
<TYPEPRIM FIX> ⇒ WORD
<TYPEPRIM FLOAT> ⇒ WORD
<TYPEPRIM SUBR> ⇒ WORD
<TYPEPRIM ATOM> ⇒ ATOM
<TYPEPRIM FORM> ⇒ LIST
6.3.4. CHTYPE
<CHTYPE any type>
("change type") returns a new object that has TYPE type and the same "data
part" as any (appendix H).
<CHTYPE (+ 2 2) FORM> ⇒ <+ 2 2>
An error is generated if the PRIMTYPE of any is not the same as the
TYPEPRIM of type. An error will also be generated if the attempted CHTYPE
is dangerous and/or senseless, for example, CHTYPEing a FIX to a SUBR.
Unfortunately, there are few useful examples we can do at this point.
[CHTYPEing a FIX to a FLOAT or vice versa produces, in general, nonsense,
since the bit formats for FIXes and FLOATs are different. The SUBRs
FIX and FLOAT convert between those formats. Useful obscurity: because of
their internal representations on the PDP-10, <CHTYPE <MAX> FIX> gives the
least possible least possible FIXFIX plus one, and analogously for
MIN.]
<CHTYPE <MIN> FIX> ⇒ 34359738367
<CHTYPE <MAX> FIX> ⇒ -34359738367
<CHTYPE [1 2] FORM> ⇒ *ERROR* STORAGE-TYPES-DIFFER
STORAGE-TYPES-DIFFER is the era's own name for the first of those errors,
and it says what went wrong exactly: a VECTOR and a FORM are not organized
the same way in storage, whatever their elements are.
Measured, MDL 55. The obscurity is real and pymdl keeps it, because a real program used it:
<CHTYPE <MIN> FIX>is the greatestFIX, and that is Zork's own "biggest number" idiom. The words behind the twoFLOATs are377777777777and400000000001octal, so the second is one more than the leastFIX(400000000000), which is why the manual's parenthesis is struck. Keeping this working at all meansCHTYPEbetweenFIXandFLOATreinterprets a genuine 36-bit PDP-10 word here, not a double -- the one place chapter 5's substrate note does not apply.
Passing note: "# notation" is just an instruction to READ saying "READ the
representation of the PRIMTYPE normally and (literally) CHTYPE it to the
specified TYPE". [Or, if the PRIMTYPE is TEMPLATE, "apply the GVAL of
the TYPE name (which should be a TEMPLATE constructor) to the given
elements of the PRIMTYPE TEMPLATE as arguments."]
Found along the way. "Literally
CHTYPEit" needs one qualification the manual does not give, and a real program found it: theCHTYPEis of a copy.#DISMISS Tin the era's own debugger must not retype theATOMTitself, and it did here until the reader was made to copy first (chapter 29).
6.4. More SUBRs Related to TYPEs
6.4.1. ALLTYPES
<ALLTYPES>
returns a VECTOR (chapter 10) containing just those ATOMs which can
currently be returned by TYPE or PRIMTYPE. This is the very "TYPE
vector" (section 22.1) that the interpreter uses: look, but don't touch. No
examples: try it, or see appendix B.
<TYPE <ALLTYPES>> ⇒ VECTOR
<1 <ALLTYPES>> ⇒ LOSE
<2 <ALLTYPES>> ⇒ FIX
<==? <ALLTYPES> <ALLTYPES>> ⇒ T
Measured, MDL 55, 2026-09-10. "The very
TYPEvector the interpreter uses" is exact, and it says more than the manual draws out: the vector is indexed by type code. Element k+1 is theTYPEwhose code is k --LOSEfirst at 0,FIXat 1 -- and the codes noTYPEis named for hold theATOMINTERNAL, twenty-one of them belowNUMPRI. Nothing in the manual says so, and nothing noticed while pymdl's vector was in an order of its own -- until the era's unassembler, which indexes it by code to name aTYPE-CODEin a listing, calledFIX's codeFLOAT(chapter 35). "Look, but don't touch" is enforced only by convention here as there: the vector is one persistent object,==?to itself, and aPUTinto it persists.
6.4.2. VALID-TYPE?
<VALID-TYPE? atom>
returns #FALSE () if atom is not the name of a TYPE, and the same object
that <TYPE-C atom> (section 19.5) returns if it is.
<VALID-TYPE? FIX> ⇒ %<TYPE-C FIX WORD>
<VALID-TYPE? NOSUCH> ⇒ #FALSE ()
6.4.3. NEWTYPE
MDL is a type-extensible language, in the sense that the programmer can invent
new TYPEs and use them in every way that the predefined TYPEs can be used.
A program-defined TYPE is called a NEWTYPE. New PRIMTYPEs cannot be
invented except by changing the interpreter; thus the TYPEPRIM of a
NEWTYPE must be chosen from those already available. But the name of a
NEWTYPE (an ATOM of course) can be chosen freely -- so long as it does not
conflict with an existing TYPE name. More importantly, the program that
defines a NEWTYPE can be included in a set of programs for manipulating
objects of the NEWTYPE in ways that are more meaningful than the predefined
SUBRs of MDL.
Typically an object of a NEWTYPE is a structure that is a model of some
entity in the real world -- or whatever world the program is concerned with --
and the elements of the structure are models of parts or aspects of the
real-world entity. A NEWTYPE definition is a convenient way of formalizing
this correspondence, of writing it down for all to see and use rather than
keeping it in your head. If the defining set of programs provides functions
for manipulating the NEWTYPE objects in all ways that are meaningful for the
intended uses of the NEWTYPE, then any other program that wants to use the
NEWTYPE can call the manipulation functions for all its needs, and it need
never know or care about the internal details of the NEWTYPE objects. This
technique is a standard way of providing modularity and abstraction.
For example, suppose you wanted to deal with airline schedules. If you were to
construct a set of programs that define and manipulate a NEWTYPE called
FLIGHT, then you could make that set into a standard package of programs and
call on it to handle all information pertaining to scheduled airline flights.
Since all FLIGHTs would have the same quantity of information (more or less)
and you would want quick access to individual elements, you would not want the
TYPEPRIM to be LIST. Since the elements would be of various TYPEs, you
would not want the TYPEPRIM to be UVECTOR -- nor its variations STRING
or BYTES. The natural choice would be a TYPEPRIM of VECTOR (although
you could gain space and lose time with TEMPLATE instead).
Now, the individual elements of a FLIGHT would, no doubt, have TYPEs and
meanings that don't change. The elements of a FLIGHT might be airline code,
flight number, originating-airport code, list of intermediate stops,
destination-airport code, type of aircraft, days of operation, etc. Each and
every FLIGHT would have the airline code for its first element (say), the
flight number for its second, and so on. It is natural to invent names
(ATOMs) for these elements and always refer to the elements by name. For
example, you could <SETG AIRLINE 1> or <SETG AIRLINE <OFFSET 1 FLIGHT>> --
and in either case <MANIFEST AIRLINE> so the compiler can generate more
efficient code. Then, if the local value of F were a FLIGHT,
<AIRLINE .F> would return the airline code, and <AIRLINE .F AA> would set
the airline code to AA. Once that is done, you can forget about which
element comes first: all you need to know are the names of the offsets.
The next step is to notice that, outside the package of FLIGHT functions, no
one needs to know whether AIRLINE is just an offset or in fact a function of
some kind. For example, the scheduled duration of a flight might not be
explicitly stored in a FLIGHT, just the scheduled times of departure and
arrival. But, if the package had the proper DURATION function for
calculating the duration, then the call <DURATION .F> could return the
duration, no matter how it is found. In this way the internal details of the
package are conveniently hidden from view and abstracted away.
The form of NEWTYPE definition allows for the TYPEs of all components of a
NEWTYPE to be declared (chapter 17), for use both by a programmer while
debugging programs that use the NEWTYPE and by the compiler for generating
faster code. It is very convenient to have the type declaration in the
NEWTYPE definition itself, rather than replicating it everywhere the
NEWTYPE is used. (If you think this declaration might be obtrusive while
debugging the programs in the NEWTYPE package, when inconsistent improvements
are being made to various programs, you can either dissociate any declaration
from the NEWTYPE or turn off MDL type-checking completely. Actually this
declaration is typically more useful to a programmer during development than it
is to the compiler.)
<NEWTYPE atom type>
returns atom, after causing it to become the representation of a brand-new
TYPE whose PRIMTYPE is <TYPEPRIM type>. What NEWTYPE actually does is
make atom a legal argument to CHTYPE and TYPEPRIM. (Note that names of new
TYPEs can be blocked lexically to prevent collision with other names, just
like any other ATOMs -- chapter 18.) Objects of a NEWTYPE-created TYPE
can be generated by creating an object of the appropriate PRIMTYPE and using
CHTYPE. They will be PRINTed (initially), and can be directly typed in, by
the use of "# notation" as described above. EVAL of any object whose TYPE
was created by NEWTYPE is initially the object itself, and, initially, you
cannot APPLY something of a generated TYPE to arguments. But see below.
Examples:
<NEWTYPE GARGLE FIX> ⇒ GARGLE
<TYPEPRIM GARGLE> ⇒ WORD
<SET A <CHTYPE 1 GARGLE>> ⇒ #GARGLE *000000000001*
<SET B #GARGLE 100> ⇒ #GARGLE *000000000144*
<TYPE .B> ⇒ GARGLE
<PRIMTYPE .B> ⇒ WORD
Found along the way. "So long as it does not conflict with an existing
TYPEname" is where the package system meets the type system, and it bites. ANEWTYPEname is anATOM, so underPACKAGEloading the same name can be created on two oblists and the two are different atoms -- and a module that tests<MEMQ .TYP <ALLTYPES>>by identity, as the compiler's own analyzer does, then fails to find its own type.ALLTYPEShere carries every atom aTYPEhas been created under, one per atom rather than one per name, appended and never reordered (chapter 34). The era did not meet this because its build was flattened, with one atom per name by default.The example's model is not hypothetical either:
<SETG AIRLINE <OFFSET 1 FLIGHT>>is chapter 17'sOFFSET, and Zork's own definers --NEWSTRUC,FLAGWORD,PSETG-- are exactly this pattern, which is why they appear in the listing program's definition file (chapter 32).
6.4.4. PRINTTYPE, EVALTYPE and APPLYTYPE
<PRINTTYPE type how>
<EVALTYPE type how>
<APPLYTYPE type how>
all return type, after specifying how MDL is to deal with it.
These three SUBRs can be used to make newly-generated TYPEs behave in
arbitrary ways, or to change the characteristics of standard MDL TYPEs.
PRINTTYPE tells MDL how to print type, EVALTYPE how to evaluate it, and
APPLYTYPE how to apply it in a FORM.
how can be either a TYPE or something that can be applied to arguments.
If how is a TYPE, MDL will treat type just like the TYPE given as how.
how must have the same TYPEPRIM as type.
If how is applicable, it will be used in the following way:
For PRINTTYPE, how should take one argument: the object being output. how
should output something without formatting (PRIN1-style); its result is
ignored. (Note: how cannot use an output SUBR on how's own type: endless
recursion will result. OUTCHAN is bound during the application to the
CHANNEL in use, or to a pseudo-internal channel for FLATSIZE -- chapter
14.) If how is the SUBR PRINT, type will receive no special treatment in
printing, that is, it will be printed as it was in an initial MDL or
immediately after its defining NEWTYPE.
For EVALTYPE, how should take one argument: the object being evaluated. The
value returned by how will be used as EVAL of the object. If how is the
SUBR EVAL, type will receive no special treatment in its evaluation.
For APPLYTYPE, how should take at least one argument. The first argument
will be the object being applied; the rest will be the objects it was given as
arguments. The result returned by how will be used as the result of the
application. If how is the SUBR APPLY, type will receive no special
treatment in application to arguments.
If any of these SUBRs is given only one argument, that is if how is omitted,
it returns the currently active how (a TYPE or an applicable object), or else
#FALSE () if type is receiving no special treatment in that operation.
Unfortunately, these examples are fully understandable only after you have read through chapter 14.
<NEWTYPE GT VECTOR> ⇒ GT
<DEFINE GP (X) <PRINC "#GT!">> ⇒ GP
<PRINTTYPE GT ,GP> ⇒ GT
<SET V <CHTYPE [1 2] GT>> ⇒ #GT [1 2]
<UNPARSE .V> ⇒ "#GT!"
<UNPARSE [.V .V]> ⇒ "[#GT! #GT!]"
<FLATSIZE .V 100> ⇒ 9
<PRINTTYPE GT ,PRINT> ⇒ GT
<PRINTTYPE GT> ⇒ #FALSE ()
<UNPARSE .V> ⇒ "#GT [1 2]"
Measured, MDL 55, 2026-08-12 and 2026-08-15. Two things in that block are not obvious from the text and were measured because they are not.
UNPARSEis "theSTRINGPRINTTYPEruns for it too -- the era answers a type's own text for anUNPARSEof it -- and so doesFLATSIZE, which is why the manual mentions a pseudo-internal channel. And the one-argument query answers the type itself for a built-in that has no customhow(<PRINTTYPE FIX>isFIX), while<EVALTYPE FIX>is#FALSE ().
The manual's own demonstration, which prints FIXes as Roman numerals:
<DEFINE ROMAN-PRINT (NUMB)
<COND (<OR <L=? .NUMB 0> <G? .NUMB 3999>>
<PRINC <CHTYPE .NUMB TIME>>)
(T
<RCPRINT </ .NUMB 1000> '![!\M]>
<RCPRINT </ .NUMB 100> '![!\C !\D !\M]>
<RCPRINT </ .NUMB 10> '![!\X !\L !\C]>
<RCPRINT .NUMB '![!\I !\V !\X]>)>>$
ROMAN-PRINT
<PRINTTYPE TIME FIX> ;"fairly harmless but necessary here"$
TIME
<PRINTTYPE FIX ,ROMAN-PRINT> ;"hee hee!"$
FIX
<+ 2 2>$
IV
1984$
MCMLXXXIV
<PRINTTYPE FIX ,PRINT>$
FIX
(RCPRINT, the ten-case helper that prints one digit's worth of numerals, is
in the manual and is omitted here for length; the whole example runs.)
And the demonstrations of EVALTYPE and APPLYTYPE:
<NEWTYPE GRITCH LIST> ⇒ GRITCH
<EVALTYPE GRITCH> ⇒ #FALSE ()
<EVALTYPE GRITCH LIST> ⇒ GRITCH
<EVALTYPE GRITCH> ⇒ LIST
#GRITCH (A <+ 1 2 3> !<SET A "ABC">) ⇒ #GRITCH (A 6 !\A !\B !\C)
<NEWTYPE HARRY VECTOR> ⇒ HARRY
<EVALTYPE HARRY #FUNCTION ((X) <1 .X>)> ⇒ HARRY
#HARRY [1 2 3 4] ⇒ 1
<NEWTYPE WINNER LIST> ⇒ WINNER
<APPLYTYPE WINNER> ⇒ #FALSE ()
<APPLYTYPE WINNER <FUNCTION (W "TUPLE" T) (!.W !.T)>> ⇒ WINNER
<APPLYTYPE WINNER> ⇒ #FUNCTION ((W "TUPLE" T) (!.W !.T))
<#WINNER (A B C) <+ 1 2> q> ⇒ (A B C 3 q)
The manual prints the GRITCH answer as and
the #GRTICH (A 6 !\A !\B !\C)APPLYTYPE query as ; both are
typographical, a transposition and a misplaced parenthesis.#FUNCTION ((W "TUPLE" T (!.W !.T))
The following sequence makes MDL look just like Lisp. (This example is understandable only if you know Lisp; it is included only because it is so beautiful.)
<EVALTYPE LIST FORM> ⇒ LIST
<EVALTYPE ATOM ,LVAL> ⇒ ATOM
(+ 1 2) ⇒ 3
(SET 'A 5) ⇒ 5
A ⇒ 5
To complete the job, of course, we would have to do some SETGs: car is 1,
cdr is ,REST, and lambda is ,FUNCTION. If you really do this example,
you should "undo" it before continuing:
<EVALTYPE LIST FORM> ⇒ LIST
<EVALTYPE ATOM ,LVAL> ⇒ ATOM
<EVALTYPE 'ATOM ,EVAL> ⇒ ATOM
<EVALTYPE LIST ,EVAL> ⇒ LIST
<+ 1 2> ⇒ 3
pymdl's own. It really is beautiful, and it really runs -- both halves, including the undoing, which needs the
'ATOMin the first restoring line because by then a bareATOMwould evaluate as anLVAL. It is also the sharpest test there is of the claim thatEVALdispatches on theTYPEand on nothing else (chapter 5): change what two of the built-in types mean and the whole reader-evaluator-printer loop becomes a different language, with no other change anywhere.