The pymdl book

5. Read, evaluate, and print

2.1. General

Once you type $ and all brackets are correctly paired and nested, the current contents of the input buffer go through processing by three functions successively: first READ, which passes its output to EVAL ("evaluate"), which passes its output to PRINT, whose output is typed on the terminal.

[Actually, the sequence is more like READ, CRLF, EVAL, PRIN1, CRLF (explained in chapter 11); MDL gives you a carriage-return line-feed when the READ is complete, that is, when all brackets are paired.]

Functionally:

  • READ: printable representations → MDL objects
  • EVAL: MDL objects → MDL objects
  • PRINT: MDL objects → printable representations

That is, READ takes ASCII text, such as is typed in at a terminal, and creates the MDL objects represented by that text. PRINT takes MDL objects, creates ASCII text representations of them, and types them out. EVAL, which is the really important one, performs transformations on MDL objects.

pymdl's own. Those are three modules here: reader.py, eval.py, printer.py, and the loop that joins them is repl.py. The three are also SUBRs an MDL program can call, exactly as the manual means them, and chapter 14 is where the manual documents them as such.

2.2. Philosophy (TYPEs)

In a general sense, when you are interacting with MDL, you are dealing with a world inhabited only by a particular set of objects: MDL objects.

MDL objects are best considered as abstract entities with abstract properties. The properties of a particular MDL object depend on the class of MDL objects to which it belongs. This class is the TYPE of the MDL object. Every MDL object has a TYPE, and every TYPE has its own peculiarities. There are many different TYPEs in MDL; they will gradually be introduced below, but in the meantime here is a representative sample: SUBR (the TYPE of READ, EVAL, and PRINT), FSUBR, LIST, VECTOR, FORM, FUNCTION, etc. Since every object has a TYPE, one often abbreviates "an object of TYPE type" by saying "a type".

The laws of the MDL world are defined by EVAL. In a very real sense, EVAL is the only MDL object which "acts", which "does something". In "acting", EVAL is always "following the directions" of some MDL object. Every MDL object should be looked upon as supplying a set of directions to EVAL; what these directions are depends heavily on the TYPE of the MDL object.

Since EVAL is so ever-present, an abbreviation is in order: "evaluates to something" or "EVALs to something" should be taken as an abbreviation for "when given to EVAL, causes EVAL to return something".

As abstract entities, MDL objects are, of course, not "visible". There is, however, a standard way of representing abstract MDL objects in the real world. The standard way of representing any given TYPE of MDL object will be given below when the TYPE is introduced. These standard representations are what READ understands, and what PRINT produces.

Measured, MDL 55, 2026-08-12. "Depends heavily on the TYPE" is exact, and exactly implemented: EVAL dispatches on an object's TYPE, not on the Python class that carries it, because the era's EVAL is a dispatch on the type word. So a LIST CHTYPEd to FORM evaluates as a form, and a user TYPE whose PRIMTYPE is one of the element-wise ones is evaluated element-wise and rebuilt under its own type, while one whose PRIMTYPE is not evaluates to itself -- which is what keeps a cyclic structure of a user type from recursing for ever. Chapters 9 and 10 have the rules.

<TYPE <EVAL <CHTYPE (+ 1 2) FORM>>>     ⇒ FIX
<EVAL <CHTYPE (+ 1 2) FORM>>            ⇒ 3
<NEWTYPE GRITCH LIST>                   ⇒ GRITCH
<EVAL <CHTYPE (A <+ 1 5>) GRITCH>>      ⇒ #GRITCH (A 6)
<NEWTYPE TXT STRING>                    ⇒ TXT
<EVAL <CHTYPE "ab" TXT>>                ⇒ #TXT "ab"

The listener's own place in this is chapter 19's, not this chapter's: the banner, the levels, and ERRET are the error handler's, and READ at a terminal is chapter 14's.

2.3. Example (TYPE FIX)

1                    ⇒ 1
<TYPE 1>             ⇒ FIX

The following has occurred:

First, READ recognized the character 1 as the representation for an object of TYPE FIX, in particular the one which corresponds to the integer one. (FIX means integer, because the decimal point is understood always to be in a fixed position: at the right-hand end.) READ built the MDL object corresponding to the decimal representation typed, and returned it.

Then EVAL noted that its input was of TYPE FIX. An object of TYPE FIX evaluates to itself, so EVAL returned its input undisturbed.

Then PRINT saw that its input was of TYPE FIX, and printed on the terminal the decimal character representation of the corresponding integer.

2.4. Example (TYPE FLOAT)

1.0                  ⇒ 1.0
<TYPE 1.0>           ⇒ FLOAT

What went on was entirely analogous to the preceding example, except that the MDL object was of TYPE FLOAT. (FLOAT means a real number (of limited precision), because the decimal point can float around to any convenient position: an internal exponent part tells where it "really" belongs.)

pymdl's own. A FLOAT is a Python double here, where the era's was a PDP-10 single-precision word: 27 bits of fraction against 52. That is platform substrate, like the machine word of chapter 21, and it is visible in exactly two places -- the precision of a printed value, and the FLOATs that pass through an 18-bit TEMPLATE field (chapter 31). The emulator of chapter 36 does carry the real 36-bit format, because compiled code computes in it.

2.5. Example (TYPE ATOM, PNAME)

GEORGE               ⇒ GEORGE
<TYPE GEORGE>        ⇒ ATOM
<SPNAME GEORGE>      ⇒ "GEORGE"

This time a lot more has happened.

READ noted that what was typed had no special meaning, and therefore assumed that it was the representation of an identifier, that is, an object of TYPE ATOM. ("Atom" means more or less indivisible.) READ therefore attempted to look up the representation in a table it keeps for such purposes [a LIST of OBLISTs, available as the local value of the ATOM OBLIST]. If READ finds an ATOM in its table corresponding to the representation, that ATOM is returned as READ's value. If READ fails in looking up, it creates a new ATOM, puts it in the table with the representation read [INSERT into <1 .OBLIST> usually], and returns the new ATOM. Nothing which could in any way be referenced as a legal "value" is attached to the new ATOM. The initially-typed representation of an ATOM becomes its PNAME, meaning its name for PRINT. One often abbreviates "object of TYPE ATOM with PNAME name" by saying "ATOM name".

EVAL, given an ATOM, returned just that ATOM.

PRINT, given an ATOM, typed out its PNAME.

At the end of this chapter, the question "what is a legal PNAME" will be considered. Further on, the methods used to attach values to ATOMs will be described.

Found along the way. "That ATOM is returned" is a stronger statement than it looks, and pymdl was wrong about it for a long time. An ATOM is an object with an identity, and two atoms that share a PNAME on different oblists are different variables with different value cells: MDL 55, measured, answers 1 to .FOO and 2 to .FOO!-INTERRUPTS after <SET FOO 1> and <SET FOO!-INTERRUPTS 2>. pymdl keyed local values by the bare name, so it had one FOO; it keys by the atom's qualified name now. Chapter 18 has the five measurements that settled it, and chapter 28 the consequences for packages.

2.6. FIXes, FLOATs, and ATOMs versus READ: Specifics

2.6.1. READ and FIXed-point Numbers

READ considers any grouping of characters which are solely digits to be a FIX, and the radix of the representation is decimal by default. A - (hyphen) immediately preceding such a grouping represents a negative FIX. The largest FIX representable on the PDP-10 is two to the 35th power minus one, or 34,359,738,367 (decimal); the smallest is one less than the negative of that number. If you attempt to type in a FIX outside that range, READ converts it to a FLOAT; if a program you write attempts to produce a FIX outside that range, an overflow error will occur (unless it is disabled).

Measured, MDL 55, 2026-08-16. The struck clause is wrong about the implementation the manual describes. Typing a FIX outside the range does not convert it to a FLOAT there; it refuses to read it:

34359738368     *ERROR* NUMBER-OUT-OF-RANGE READ
-34359738368    *ERROR* NUMBER-OUT-OF-RANGE READ

-- the second because the digits are read as a magnitude first, and 2^35 is already past the largest FIX, so the smallest FIX cannot be typed in decimal at all (chapter 21 has the octal that reaches it). 34359738367 and -34359738367 read as FIXes and 1e20 reads as a FLOAT, so it is the integer path alone that refuses. pymdl followed the manual and answered 3.4359738E+10 until that was measured; where the manual and the era disagree, this project follows the era.

34359738367          ⇒ 34359738367
-34359738367         ⇒ -34359738367
34359738368          ⇒ *ERROR* NUMBER-OUT-OF-RANGE
<TYPE 1e20>          ⇒ FLOAT

The radix used by READ and PRINT is changeable by the user; however, there are two formats for representations of FIXes which cause READ to use a specified radix independent of the current one. These are as follows:

  1. If a group of digits is immediately followed by a period (.), READ interprets that group as the decimal representation of a FIX. For example, 10. is always interpreted by READ as the decimal representation of ten.
  2. If a group of digits is immediately enclosed on both sides with asterisks (*), READ interprets that group as the octal representation of a FIX. For example, *10* is always interpreted by READ as the octal representation of eight.
10.                  ⇒ 10
*10*                 ⇒ 8
*377*                ⇒ 255

Measured, MDL 55, 2026-08-16. The changeable radix is not symmetric, and the asymmetry is arithmetic rather than tabular. PRINT's digits are '0' + d with no letter table, so a radix above ten prints characters that are not digits at all; READ's digits are 0 to 9 at every radix, so it cannot read those characters back.

<UNPARSE 10 16>          ⇒ ":"
<UNPARSE 26 16>          ⇒ "1:"
<UNPARSE 255 16>         ⇒ "??"
<TYPE <PARSE "FF" 16>>   ⇒ ATOM
<TYPE <PARSE "1:" 16>>   ⇒ ATOM

ASCII 0 is 060 octal, so ten lands on : and fifteen on ?. A radix above ten is therefore print-only on both machines: the era can print a FIX it cannot read back. And the reading arithmetic checks nothing at all -- not that a digit is below the radix, not that the radix is sane:

<PARSE "123" 3>      ⇒ 18
<PARSE "123" 1>      ⇒ 6
<PARSE "123" 0>      ⇒ 3

which is val = val*radix + (ch - '0') run over the token and nothing else: for radix 3, 1*3+2 is 5 and 5*3+3 is 18, the digit 3 taken though it is not a digit in radix 3. pymdl clamped a radix below two up to ten and rejected an out-of-range digit until this was measured.

2.6.2. READ and PRINT versus FLOATing-point Numbers

PRINT can produce, and READ can understand, two different formats for objects of TYPE FLOAT. The first is "decimal-point" notation, the second is "scientific" notation. Decimal radix is always used for representations of FLOATs.

"Decimal-point" notation for a FLOAT consists of an arbitrarily long string of digits containing one . (period) which is followed by at least one digit. READ will make a FLOAT out of any such object, with a limit of precision of one part in 2 to the 27th power.

"Scientific" notation consists of:

  1. a number,
  2. immediately followed by E or e (upper or lower case letter E),
  3. immediately followed by an exponent,

where a "number" is an arbitrarily long string of digits, with or without a decimal point (see following note), and an "exponent" is up to two digits worth of FIX. This notation represents the "number" to the "exponent" power of ten. Note: if the "number" as above would by itself be a FIX, and if the "exponent" is positive, and if the result is within the allowed range of FIXes, then the result will be a FIX. For example, READ understands 10E1 as 100 (a FIX), but 10E-1 as 1.0000000 (a FLOAT).

The largest-magnitude FLOAT which can be handled without overflow is 1.7014118E+38 (decimal radix). The smallest-magnitude FLOAT which can be handled without underflow is .14693679E-38.

1.2345               ⇒ 1.2345000
10E1                 ⇒ 100
<TYPE 10E1>          ⇒ FIX
10E-1                ⇒ 1.0
<TYPE 10E-1>         ⇒ FLOAT

pymdl's own. 1.2345 prints as 1.2345000 because the printer writes the era's seven significant figures, which is what a 27-bit fraction is worth; the value behind it is a double (2.4). The two magnitude limits are the PDP-10 single-precision format's and are not enforced here, since a double reaches far past both.

2.6.3. READ and PNAMEs

The question "what is a legal PNAME?" is actually not a reasonable one to ask: any non-empty string of arbitrary characters can be the PNAME of an ATOM. However, some PNAMEs are easier to type to READ than others. But even the question "what are easily typed PNAMEs?" is not too reasonable, because READ decides that a group of characters is a PNAME by default; if it can't possibly be anything else, it's a PNAME. So, the rules governing the specification of PNAMEs are messy, and best expressed in terms of what is not a PNAME. For simplicity, you can just consider any uninterrupted group of upper- and lower-case letters and (customarily) hyphens to be a PNAME; that will always work. If you are neither a perfectionist nor a masochist, skip to the next chapter.

2.6.3.1. Non-PNAMEs

A group of characters is not a PNAME if:

  1. It represents a FLOAT or a FIX, as described above -- that is, it is composed wholly of digits, or digits and a single . (period), or digits and a . and the letter E or e (with optional minus signs in the right places).
  2. It begins with a . (period).
  3. It contains -- if typed interactively -- any of the characters which have special interactive effects: ^@, ^D, ^L, ^G, ^O, $ (ESC), rubout.
  4. It contains a format character -- space, carriage-return, line-feed, form-feed, horizontal tab, vertical tab.
  5. It contains a , (comma) or a # (number sign) or a ' (single quote) or a ; (semicolon) or a % (percent sign).
  6. It contains any variety of bracket -- ( or ) or [ or ] or < or > or { or } or ".

In addition, the character \ (backslash) has a special interpretation, as mentioned below. Also the pair of characters !- (exclamation-point hyphen) has an extremely special interpretation, which you will reach at chapter 18.

The characters mentioned in cases 4 through 6 are "separators" -- that is, they signal to READ that whatever it was that the preceding characters represented, it's done now. They can also indicate the start of a new object's representation (all the opening "brackets" do just that).

Measured, MDL 55, 2026-08-16. Case 3 is about interactive effects, but one of those characters separates tokens wherever it is read, not only at a terminal: ESC. Reading 1<ESC>C from a file channel, the era gives the FIX 1 and then the ATOM C; reading 1^AC from the same channel gives the single ATOM 1C, so the other control characters do not break a token there. ESC is the activation character, and the editor's macro facility depends on its separating ("put an ESC between commands", chapter 29).

2.6.3.2. Examples

The following examples are not in the "standard format" of "line typed in$ result printed", because they are not, in some cases, completed objects; hence READ would continue waiting for the brackets to be closed. In other cases, they will produce errors during EVALuation if other -- currently irrelevant -- conditions are not met. Instead, the right-hand column states just what READ thought the input in the left-hand column really was.

input explanation
ABC$ an ATOM of PNAME ABC
abc$ an ATOM of PNAME abc
ARBITRARILY-LONG-PNAME$ an ATOM of PNAME ARBITRARILY-LONG-PNAME
1.2345$ a FLOAT, PRINTed as 1.2345000
1.2.345$ an ATOM of PNAME 1.2.345
A.or.B$ an ATOM of PNAME A.or.B
.A.or.B$ not an ATOM, but (as explained later) a FORM containing an ATOM of PNAME A.or.B
MORE THAN ONE$ three ATOMs, with PNAMEs MORE, and THAN, and ONE
ab(cd$ an ATOM of PNAME ab, followed by the start of something else (the something else will contain an ATOM of PNAME beginning cd)
12345A34$ an ATOM of PNAME 12345A35 12345A34 (if the A had been an E, the object would have been a FLOAT)

The struck 12345A35 is a typographical error in the print: the input has a 4 at the end.

<TYPE 1.2.345>            ⇒ ATOM
<SPNAME 1.2.345>          ⇒ "1.2.345"
<TYPE A.or.B>             ⇒ ATOM
<TYPE 12345A34>           ⇒ ATOM
<SPNAME 12345A34>         ⇒ "12345A34"
<TYPE '.A.or.B>           ⇒ FORM
<TYPE <1 '.A.or.B>>       ⇒ ATOM
<SPNAME <2 '.A.or.B>>     ⇒ "A.or.B"

2.6.3.3. \ (Backslash) in ATOMs

If you have a strange, uncontrollable compulsion to have what were referred to as "separators" above as part of the PNAMEs of your ATOMs, you can do so by preceding them with the character \ (backslash). \ will also magically turn an otherwise normal FIX or FLOAT into an ATOM if it appears amongst the digits. In fact, backslash in front of any character changes it from something special to "just another character" (including the character \). It is an escape character.

When PRINT confronts an ATOM which had to be backslashed in order to be an ATOM, it will dutifully type out the required \s. They will not, however, necessarily be where you typed them; they will instead be at those positions which will cause READ the least grief. For example, PRINT will type out a PNAME which consists wholly of digits by first typing a \ and then typing the digits -- no matter where you originally typed the \ (or \s).

2.6.3.4. Examples of Awful ATOMs

The following examples illustrate the amount of insanity that can be perpetrated by using \. The format of the examples is again non-standard, this time not because anything is unfinished or in error, but because commenting is needed: PRINT doesn't do it full justice.

input explanation
a\ one\ and\ a\ two$ one ATOM, whose PNAME has four spaces in it
1234\56789$ an ATOM of PNAME 123456789, which PRINTs as \123456789
123\ $ an ATOM of PNAME 123space, which PRINTs as \123\ , with a space on the end
\\$ an ATOM whose PNAME is a single backslash
<LENGTH <SPNAME a\ one\ and\ a\ two>>   ⇒ 15
<SPNAME 1234\56789>                     ⇒ "123456789"
<SPNAME \\>                             ⇒ "\\"
<TYPE 1234\56789>                       ⇒ ATOM

(<SPNAME \\> answers a one-character STRING holding a backslash; the printed representation doubles it, as a STRING's printed representation must.)

pymdl's own. The printer's rule -- put the backslashes where READ will have least grief, not where they were typed -- is implemented, and so is the harder half of it: an ATOM printed at a moment when the oblist path does not reach its home gets the minimal trailer that would read it back (chapter 18), which is a property of the path and not of the atom. The reader is the place where an interpreter can silently stop being faithful, so it is measured hard: the manual's own examples are tests, the era batteries of chapter 37 compared 710 forms against MDL 55, and the read tables and macro characters of chapter 20 have their own.