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 TYPE and 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's TYPE-CODE. They are pinned to MDL 55's own census all the same, every built-in TYPE, 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.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 FIX least possible FIX 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 greatest FIX, and that is Zork's own "biggest number" idiom. The words behind the two FLOATs are 377777777777 and 400000000001 octal, so the second is one more than the least FIX (400000000000), which is why the manual's parenthesis is struck. Keeping this working at all means CHTYPE between FIX and FLOAT reinterprets 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 CHTYPE it" needs one qualification the manual does not give, and a real program found it: the CHTYPE is of a copy. #DISMISS T in the era's own debugger must not retype the ATOM T itself, and it did here until the reader was made to copy first (chapter 29).

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 TYPE vector 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 the TYPE whose code is k -- LOSE first at 0, FIX at 1 -- and the codes no TYPE is named for hold the ATOM INTERNAL, twenty-one of them below NUMPRI. 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 a TYPE-CODE in a listing, called FIX's code FLOAT (chapter 35). "Look, but don't touch" is enforced only by convention here as there: the vector is one persistent object, ==? to itself, and a PUT into 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 TYPE name" is where the package system meets the type system, and it bites. A NEWTYPE name is an ATOM, so under PACKAGE loading 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. ALLTYPES here carries every atom a TYPE has 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's OFFSET, 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. UNPARSE is "the STRING PRINT would have produced", so a custom PRINTTYPE runs for it too -- the era answers a type's own text for an UNPARSE of it -- and so does FLATSIZE, 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 custom how (<PRINTTYPE FIX> is FIX), 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 #GRTICH (A 6 !\A !\B !\C) and the APPLYTYPE query as #FUNCTION ((W "TUPLE" T (!.W !.T)); both are typographical, a transposition and a misplaced parenthesis.

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 'ATOM in the first restoring line because by then a bare ATOM would evaluate as an LVAL. It is also the sharpest test there is of the claim that EVAL dispatches on the TYPE and 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.