The pymdl book

19. Errors, frames, etc.

16.1. LISTEN

This SUBR takes any number of arguments. It first checks the LVALs of INCHAN, OUTCHAN, and OBLIST for reasonability and terminal usability. In each case, if the value is unreasonable, the ATOM is rebound to the corresponding GVAL, if reasonable, or to an invented reasonable value. LISTEN then does <TTYECHO .INCHAN T> and <ECHOPAIR .INCHAN .OUTCHAN>. Next, it PRINTs its arguments, then PRINTs

LISTENING-AT-LEVEL i PROCESS p

where i is an integer (FIX) which is incremented each time LISTEN is called recursively, and p is an integer identifying the PROCESS (chapter 23) in which the LISTEN was EVALed. LISTEN then does <APPLY <VALUE REP>>, if there is one, and if it is APPLICABLE. If not, it applies the SUBR REP (without making a new FRAME -- see below). This SUBR drops into an infinite READ-EVAL-PRINT loop, which can be left via ERRET (section 16.4).

The standard LISTEN loop has two features for getting a handle on objects that you have typed in and MDL has typed out. If the ATOM L-INS has a local value that is a LIST, LISTEN will keep recent inputs (what READ returns) in it, most recent first. Similarly, if the ATOM L-OUTS has a local value that is a LIST, LISTEN will keep recent outputs (what EVAL returns) in it, most recent first. The keeping is done before the PRINTing, so that ^S does not defeat its purpose. The user can decide how much to keep around by setting the length of each LIST. Even if L-OUTS is not used, the atom LAST-OUT is always SET to the last object returned by EVAL in the standard LISTEN loop. Example:

<SET L-INS (NEWEST NEWER NEW)>$
(NEWEST NEWER NEW)
.L-INS$
(.L-INS NEWEST NEWER)
<SET FOO 69>$
69
<SET FIXIT <2 .L-INS>>  ;"grab the last input"$
<SET FOO 69>
.L-INS$
(.L-INS <SET FIXIT <2 .L-INS>> <SET FOO 69>)
<PUT .FIXIT 3 105>$
<SET FOO 105>
<EVAL .FIXIT>$
105
.L-INS$
(.L-INS <EVAL .FIXIT> <PUT .FIXIT 3 105>)
.FOO$
105

That transcript is shown rather than run, because it is a conversation with a listener rather than a sequence of evaluations -- .L-INS only fills up because a listener is reading the lines. It is worth reading closely all the same: the LIST never grows, each new input pushes the oldest off the end, and FIXIT ends up holding the actual FORM that was typed, so PUTting into it edits a command you already gave and EVAL runs the edited version. That is a command history you can PUT into, in 1979, in eight SUBRs.

pymdl's own. The listener here is the same loop, including the level counter and the PROCESS number in the banner, because chapter 37 needs pymdl's banners to be comparable with MDL 55's word for word. What differs is the terminal: ^S and ^G below are real keys on a real terminal, and a pymdl session is as often a pipe. Chapter 2 has what the driver does instead, and chapter 29's debugger is a program that drives this loop deliberately.

16.2. ERROR

This SUBR is the same as LISTEN, except that (1) it generates an interrupt (chapter 24), if enabled, and (2) it PRINTs *ERROR* before PRINTing its arguments.

When any SUBR or FSUBR detects an anomalous condition (for example, its arguments are of the wrong TYPE), it calls ERROR with at least two arguments, including:

  1. an ATOM whose PNAME describes the problem, normally from the OBLIST ERRORS!- (appendix C),
  2. the ATOM that names the SUBR or FSUBR, and
  3. any other information of interest,

and then returns whatever the call to ERROR returns. Exception: a few (for example DEFINE) will take further action that depends on the value returned. This nonstandard action is specified in the error message (first ERROR argument).

Point 1 is literally true, and it is the reason chapter 18 listed ERRORS!- among the initial OBLISTs:

<GET <ERRORS> OBLIST>                      ⇒ ERRORS
<LOOKUP "OUT-OF-BOUNDS" <ERRORS>>          ⇒ OUT-OF-BOUNDS!-ERRORS
<LOOKUP "NOT-AN-ERROR-NAME" <ERRORS>>      ⇒ #FALSE ()

An error message is not a string; it is an ATOM on a particular OBLIST, which is why a program can compare against it and why the trailer prints.

Measured, MDL 55. Which ATOM, and which extra arguments, is the single most heavily measured thing in this project. The banner a SUBR prints is the most visible way an interpreter can be subtly wrong, so pymdl's table of them was not derived from this chapter or from appendix C -- every entry was obtained by running the offending form through MDL 55 under the emulator and through pymdl and diffing the output, over 720 generated forms. Chapter 6's box has what that turned up about how irregular the era's own choices are, chapter 37 has how the measuring is done, and appendix E has the differences that remain.

16.3. FRAME (the TYPE)

A FRAME is the object placed on a PROCESS's control stack (chapter 23) whenever a SUBR, FSUBR, RSUBR, or RSUBR-ENTRY (chapter 33) is applied. (These objects are herein collectively called "Subroutines".) It contains information describing what was applied, plus a TUPLE whose elements are the arguments to the Subroutine applied. If any of the Subroutine's arguments are to be evaluated, they will have been by the time the FRAME is generated.

A FRAME is an anomalous TYPE in the following ways:

  1. It cannot be typed in. It can be generated only by applying a Subroutine.
  2. It does not type out in any standard format, but rather as #FRAME followed by the PNAME of the Subroutine applied.

16.3.1. ARGS

<ARGS frame>

("arguments") returns the argument TUPLE of frame.

16.3.2. FUNCT

<FUNCT frame>

("function") returns the ATOM whose G/LVAL is being applied in frame.

16.3.3. FRAME (the SUBR)

<FRAME frame>

returns the FRAME stacked before frame or, if there is none, it will generate an error. The oldest (lowest) FRAME that can be returned without error has a FUNCT of TOPLEVEL. If called with no arguments, FRAME returns the topmost FRAME used in an application of ERROR or LISTEN, which was bound by the interpreter to the ATOM LERR\ !-INTERRUPTS ("last error").

<TYPE <FRAME>>             ⇒ FRAME
<FUNCT <FRAME>>            ⇒ LISTEN
<TYPE <ARGS <FRAME>>>      ⇒ TUPLE

LERR\ !-INTERRUPTS is the third of the interpreter's own backslashed ATOMs, after LPROG\ and LMAP\ in chapter 13, and it works the same way: an ordinary local value on the INTERRUPTS oblist, rebound by each ERROR or LISTEN.

16.3.4. Examples

Say you have gotten an error. You can now type at ERROR's LISTEN loop and get things EVALed. For example,

<FUNCT <FRAME>>$
ERROR
<FUNCT <FRAME <FRAME>>>$
the-name-of-the-Subroutine-which-called-ERROR:atom
<ARGS <FRAME <FRAME>>>$
the-arguments-to-the-Subroutine-which-called-ERROR:tuple

16.4. ERRET

<ERRET any frame>

This SUBR ("error return") (1) causes the control stack to be stripped down to the level of frame, and (2) then returns any. The net result is that the application which generated frame is forced to return any. Additional side effects that would have happened in the absence of an error may not have happened.

The second argument to ERRET is optional, by default the FRAME of the last invocation of ERROR or LISTEN.

If ERRET is called with no arguments, it drops you all the way down to the bottom of the control stack -- before the level-1 LISTEN loop -- and then calls LISTEN. As always, LISTEN first ensures that MDL is receptive.

Examples:

<* 3 <+ a 1>>$
*ERROR*
ARG-WRONG-TYPE
+
LISTENING-AT-LEVEL 2 PROCESS 1
<ARGS <FRAME <FRAME>>>$
[a 1]
<ERRET 5>$      ;"This causes the + to return 5."
15              ;"finally returned by the *"

Note that when you are in a call to ERROR, the most recent set of bindings is still in effect. This means that you can examine values of dummy variables while still in the error state. For example,

<DEFINE F (A "AUX" (B "a string"))
        #DECL ((VALUE) LIST (A) STRUCTURED (B) STRING)
        (.B <REST .A 2>)        ;"Return this LIST.">$
F
<F '(1)>$

*ERROR*
OUT-OF-BOUNDS
REST
LISTENING-AT-LEVEL 2 PROCESS 1
.A$
(1)
.B$
"a string"
<ERRET '(5)>    ;"Make the REST return (5)."$
("a string" (5))

Both transcripts are shown rather than run, since each is a dialogue with an error listener. The second is the more important one to understand: after the error, .A and .B still answer, because the error did not unwind anything -- ERROR is an ordinary Subroutine call stacked on top of the failing one. That is what makes MDL's error handling a debugger rather than a report.

pymdl's own. The whole of section 16.4 works here, and chapter 29 is where it is put to work: the debugger of the environment manual is built out of FRAME, ARGS, FUNCT, ERRET and RETRY and nothing else. Driving an error listener from a script rather than a terminal is what tools/mdl55_errhandler.py exists for on the measuring side, since a stateful error handler is the only way to ask MDL 55 seven hundred questions without restarting it seven hundred times (chapter 37).

16.5. RETRY

<RETRY frame>

causes the control stack to be stripped down just beyond frame, and then causes the Subroutine call that generated frame to be done again. frame is optional, by default the FRAME of the last invocation of ERROR or LISTEN. RETRY differs from AGAIN in that (1) it is not intended to be used in programs; (2) it can retry any old frame (any Subroutine call), whereas AGAIN requires an ACTIVATION (PROG or REPEAT or "ACT"); and (3) if it retries the EVAL of a FORM that makes an ACTIVATION, it will cause rebinding in the argument LIST, thus duplicating side effects.

16.6. UNWIND

UNWIND is an FSUBR that takes two arguments, usually FORMs. It EVALs the first one, and, if the EVAL returns normally, the value of the EVAL call is the value of UNWIND. If, however, during the EVAL a non-local return attempts to return below the UNWIND FRAME in the control stack, the second argument is EVALed, its value is ignored, and the non-local return is completed. The second argument is evaluated in the environment that was present when the call to UNWIND was made. This facility is useful for cleaning up data bases that are in inconsistent states and for closing temporary CHANNELs that may be left around. FLOAD sets up an UNWIND to close its CHANNEL if the user attempts to ERRET without finishing the FLOAD. Example:

<DEFINE CLEAN ACT ("AUX" (C <OPEN "READ" "A FILE">))
    #DECL ((C) <OR CHANNEL FALSE> ...)
    <COND (.C
            <UNWIND <PROG () ... <CLOSE .C>>
                    <CLOSE .C>>)>>

Both halves are testable, and both hold:

<SETG CLEANED <>>          ⇒ #FALSE ()
<UNWIND <+ 1 2> <SETG CLEANED T>>      ⇒ 3
,CLEANED                   ⇒ #FALSE ()

-- a normal return gives the first argument's value and the cleanup does not run. And:

<SETG CLEANED <>>          ⇒ #FALSE ()
<PROG OUT () <UNWIND <RETURN 9 .OUT> <SETG CLEANED T>>>    ⇒ 9
,CLEANED                   ⇒ T

-- a RETURN past the UNWIND runs the cleanup and then completes the return. It reaches across a function call too, which is the case that matters for the CLEAN above:

<SETG CLEANED <>>          ⇒ #FALSE ()
<DEFINE INNER (A) <RETURN 7 .A>>       ⇒ INNER
<DEFINE OUTER ACT () <UNWIND <INNER .ACT> <SETG CLEANED T>>>   ⇒ OUTER
<OUTER>                    ⇒ 7
,CLEANED                   ⇒ T

16.7. Control-G (^G)

Typing control-G (^G, <ASCII 7>) at MDL causes it to act just as if an error had occurred in whatever was currently being done. You can then examine the values of variables as above, continue by applying ERRET to one argument (which is ignored), RETRY a FRAME lower on the control stack, or flush everything by applying ERRET to no arguments.

16.8. Control-S (^S)

Typing control-S (^S, <ASCII 19>) at MDL causes it to stop what is happening and return to the FRAME .LERR\ !-INTERRUPTS, returning the ATOM T. (In the Tenex and Tops-20 versions, ^O also has the same effect.)

These are the two keys chapter 10 told you to know before trying its self-referencing structures, and now they can be stated exactly: ^G stops and keeps the stack so you can look around, ^S stops and abandons the computation back to the last listener.

16.9. OVERFLOW

<OVERFLOW false-or-any>

There is one error that can be disabled: numeric overflow and underflow caused by the arithmetic SUBRs (+, -, *, /). The SUBR OVERFLOW takes one argument: if it is of TYPE FALSE, under/overflow errors are disabled; otherwise they are enabled. The initial state is enabled. OVERFLOW returns T or #FALSE (), reflecting the previous state. Calling it with no argument returns the current state.

<OVERFLOW>                 ⇒ T
<* 34359738367 2>          ⇒ *ERROR* ARITHMETIC-OVERFLOW
<OVERFLOW <>>              ⇒ T
<OVERFLOW>                 ⇒ #FALSE ()
<* 34359738367 2>          ⇒ -2
<OVERFLOW T>               ⇒ #FALSE ()

pymdl's own. With overflow disabled the answer is -2, which is the PDP-10 word wrapping, not Python's unbounded integers. That is deliberate and it is the whole reason this section is interesting here: chapter 9 established that a FIX is 36 bits, and this section is the only documented way to see the wrap-around a 36-bit machine actually did. pymdl therefore carries the era's word width through +, -, * and / rather than letting a big integer escape. Chapter 21's WORD arithmetic is the same width made explicit, and chapter 36 is where the words are real.

"The initial state is enabled" is also why the arithmetic of chapter 6 signals rather than silently growing: an era program that relies on overflow being an error gets an error.