The pymdl book
23. Interrupts
The MDL interrupt handling facilities provide the ability to say the following:
whenever "this event" occurs, stop whatever is being done at the time and perform
"this action"; when "this action" is finished, continue with whatever was
originally being done. "This event" can be things like the typing of a character
at a terminal, a time interval ending, a PROCESS becoming blocked, or a
program-defined and -generated "event". "This action" is the application of a
specified APPLICABLE object to arguments provided by the MDL interrupt system.
The sets of events and actions can be changed in extremely flexible ways, which
accounts for both the variety of SUBRs and arguments, and the rich interweaving
of the topics in this chapter. Interrupt handling is a kind of parallel
processing: a program can be divided into a "main-level" part and one or more
interrupt handlers that execute only when conditions are ripe.
pymdl's own. The interrupt interface comprises registration, priorities, handlers, and delivery. An interrupt also needs an event source: registering a name alone does not cause it to occur. pymdl supplies sources for events such as terminal input, timers, garbage collection, and IPC messages. IPC uses Unix sockets, as described in chapter 25. The historical descriptions below also include ITS system signals and hardware faults; their presence in the manual does not imply that the host generates them in pymdl.
21.1. Definitions of Terms
An interrupt is not an object in MDL, but rather a class of events, for example, "ticks" of a clock, garbage collections, the typing of a character at a terminal, etc.
An interrupt is said to occur when one of the events in its class takes place.
An external interrupt is one whose occurrences are signaled to MDL by the operating system, for example, "ticks" of a clock. An internal interrupt is one whose occurrences are detected by MDL itself, for example, garbage collections. MDL can arrange for the operating system to not signal occurrences of an external interrupt to it; then, as far as MDL is concerned, that interrupt does not occur.
Each interrupt has a name which is either a STRING (for example, "GC",
"CHAR", "WRITE") or an ATOM with that PNAME in a special OBLIST, named
INTERRUPTS!-. (This OBLIST is returned by <INTERRUPTS>.) Certain names
must always be further specified by a CHANNEL or a LOCATIVE to tell which
interrupt by that name is meant.
<GET <INTERRUPTS> OBLIST> ⇒ INTERRUPTS
That is the third of chapter 18's initial OBLISTs, and now it has a purpose.
When an interrupt occurs, the interpreter looks for an association on the
interrupt's name. If there is an association, its AVALUE should be an
IHEADER, which heads a list of actions to be performed. In each IHEADER is
the name of the interrupt with which the IHEADER is or was associated.
That association is the one chapter 16's box found sitting in a fresh interpreter, before any program has done anything:
<TYPE <ASSOCIATIONS>> ⇒ ASOC
<INDICATOR <ASSOCIATIONS>> ⇒ INTERRUPT
In each IHEADER is an element telling whether it is disabled. If an IHEADER
is disabled, then none of its actions is performed. The opposite of disabled
is enabled. It is sometimes useful to disable an IHEADER temporarily, but
removing its association with the interrupt's name is better than long-term
disabling. There are SUBRs for creating an IHEADER, associating it with an
interrupt, and later removing the association.
In each IHEADER is a priority, a FIX greater than 0 which specifies the
interrupt's "importance". The processing of a higher-priority (larger-numbered)
interrupt will supersede the processing of a lower-priority (smaller-numbered)
interrupt until the high-priority interrupt has been handled.
In each IHEADER is a (possibly empty) list of HANDLERs. (This list is not a
MDL LIST.) Each HANDLER corresponds to an action to perform. There are
SUBRs for creating a HANDLER, adding it to an IHEADER's list, and later
removing it.
In each HANDLER is a function that we will call a handler (in lower case),
despite possible confusion, because that is really the best name for it. An
action consists of applying a handler to arguments supplied by the interrupt
system. The number and meaning of the arguments depend on the name of the
interrupt. In each HANDLER is an element telling in which PROCESS the action
should be performed.
21.2. EVENT
<EVENT name priority which>
creates and returns an enabled IHEADER with no HANDLERs. The name may be an
ATOM in the INTERRUPTS OBLIST or a STRING; if it is a STRING, EVENT
does a LOOKUP or INSERT in <INTERRUPTS>. If there already is an IHEADER
associated with name, EVENT just returns it, ignoring the given priority.
which must be given only for certain names:
- It must be a
CHANNELif and only if name is"CHAR"(orCHAR!-INTERRUPTS). In this case it is the inputCHANNELfrom the (pseudo-)terminal or Network socket whose received characters will cause the interrupt to occur, or the outputCHANNELto the pseudo-terminal or Network socket whose desired characters will cause the interrupt to occur. - The argument must be a
LOCATIVEif and only if name is"READ"(orREAD!-INTERRUPTS) or"WRITE"(orWRITE!-INTERRUPTS). In this case it specifies an object to be "monitored" for usage by (interpreted) MDL programs (section 21.8.9).
If the interrupt is external, MDL arranges for the operating system to signal its occurrences.
<TYPE <EVENT "MY-EVENT" 5>> ⇒ IHEADER
<EVENT "MY-EVENT" 5> ⇒ #IHEADER MY-EVENT!-INTERRUPTS
<==? <EVENT "MY-EVENT" 5> <EVENT "MY-EVENT" 99>> ⇒ T
The last line is "EVENT just returns it, ignoring the given priority": asking
twice gets the same IHEADER, not a second one with a new priority. Note also
that the STRING became an ATOM on the INTERRUPTS oblist, and the printed
trailer says so.
21.3. HANDLER (the SUBR)
<HANDLER iheader applicable process>
creates a HANDLER, adds it to the front of iheader's HANDLER list (first
action to be performed), and returns it as a value. applicable may be any
APPLICABLE object that takes the proper number of arguments. (None of the
arguments can be QUOTEd; they must all be evaluated at call time.) process is
the PROCESS in which the handler will be applied, by default whatever PROCESS
was running when the interrupt occurred.
The value returned by the handler is ignored, unless it is of TYPE DISMISS
(PRIMTYPE ATOM), in which case none of the remaining actions in the list will
be performed.
The processing of an interrupt's actions can terminate prematurely if a handler
calls the SUBR DISMISS (see below).
"Front of the list" is observable, since both handlers run:
<SETG N 0> ⇒ 0
<ON "MY-EVENT" <FUNCTION (X) <SETG N <+ ,N 1>>> 5>
<ON "MY-EVENT" <FUNCTION (X) <SETG N <+ ,N 10>>> 5>
<INTERRUPT "MY-EVENT" 0> ⇒ T
,N ⇒ 11
21.4. OFF
<OFF iheader>
removes the association between iheader and the name of its interrupt, and then disables iheader and returns it. (An error occurs if there is no association.) If the interrupt is external, MDL arranges for the operating system not to signal its occurrences.
<OFF name which>
finds the IHEADER associated with name and proceeds as above, returning the
IHEADER. which must be given only for certain names, as for EVENT. Caution:
if you <OFF "CHAR" ,INCHAN>, MDL will become deaf.
<OFF handler>
returns handler after removing it from its list of actions. There is no effect
on any other HANDLERs in the list.
Now that you know how to remove IHEADERs and HANDLERs from their normal
places, you need to know how to put them back:
<EVENT iheader>
If iheader was previously disabled or disassociated from its name, EVENT will
associate and enable it.
<HANDLER iheader handler>
If handler was previously removed from its list, HANDLER will add it to the
front of iheader's list of actions. Note that process cannot be specified.
<SETG FIRED <>> ⇒ #FALSE ()
<SETG H <ON "MY-EVENT" <FUNCTION (X) <SETG FIRED .X>> 5>>
<INTERRUPT "MY-EVENT" 42> ⇒ T
,FIRED ⇒ 42
<OFF ,H> ⇒ #HANDLER #FUNCTION ((X) <SETG FIRED .X>)
<SETG FIRED <>> ⇒ #FALSE ()
<INTERRUPT "MY-EVENT" 3> ⇒ T
,FIRED ⇒ #FALSE ()
Note the last two lines carefully: INTERRUPT still answers T, because the
IHEADER is still enabled -- it just has no actions left. Section 21.9 says
exactly this: "even if INTERRUPT returns T, it is possible that nothing
'really happened'."
21.5. IHEADER and HANDLER (the TYPEs)
Both these TYPEs are of PRIMTYPE VECTOR, but they do not PRINT that way,
since they are self-referencing. Instead they PRINT as
#type most-interesting-component
<TYPEPRIM IHEADER> ⇒ VECTOR
<TYPEPRIM HANDLER> ⇒ VECTOR
"Since they are self-referencing" is chapter 10's section 7.8 with a practical
consequence: an IHEADER points at its HANDLERs and each HANDLER points back,
so printing one in full would never finish. The abbreviated form is the
interpreter declining to try.
The contents of IHEADERs and HANDLERs can be changed by PUT, and the new
values will then determine the behavior of MDL.
Before describing the elements of these TYPEs in detail, here are a picture and
a Pattern, both purporting to show how they look:
#IHEADER [name:atom or which
disabled?
*-----------> #HANDLER [*-----------> #HANDLER [#HANDLER []
priority] <-------------* +------*
applicable | applicable
process] <-------+ process]
<IHEADER <OR ATOM CHANNEL LOCATIVE>
<OR '#LOSE 0 '#LOSE -1>
<HANDLER HANDLER <OR HANDLER IHEADER> APPLICABLE PROCESS>
FIX>
21.5.1. IHEADER
The elements of an IHEADER are as follows:
- name of interrupt (
ATOM, orCHANNELif the name is"CHAR", orLOCATIVEif the name is"READ"or"WRITE") - non-zero if and only if disabled
- first
HANDLER, if any, else a zero-lengthHANDLER - priority
All four, read out of a real one:
<LENGTH <EVENT "MY-EVENT" 5>> ⇒ 4
<1 <EVENT "MY-EVENT" 5>> ⇒ MY-EVENT!-INTERRUPTS
<2 <EVENT "MY-EVENT" 5>> ⇒ 0
<3 <EVENT "MY-EVENT" 5>> ⇒ #HANDLER []
<4 <EVENT "MY-EVENT" 5>> ⇒ 5
<DISABLE <EVENT "MY-EVENT" 5>>
<2 <EVENT "MY-EVENT" 5>> ⇒ -1
<ENABLE <EVENT "MY-EVENT" 5>>
<2 <EVENT "MY-EVENT" 5>> ⇒ 0
#HANDLER [] is the "zero-length HANDLER" of item 3 -- a real object standing
in for "no handlers", so the list never contains a FALSE. And the disabled
flag really is 0 and -1, which is what makes section 21.6's remark about
PUT accurate rather than metaphorical.
If you lose track of an IHEADER, you can get it via the association:
- For
"CHAR"interrupts,<GET channel INTERRUPT>returns theIHEADERor#FALSE ()if there is no association;<EVENT "CHAR" 0 channel>returns theIHEADER, creating it if there is no association. - For
"READ"interrupts,<GET locative READ!-INTERRUPTS>returns theIHEADERor#FALSE ();<EVENT "READ" 0 locative>returns it, creating it if there is no association. - For
"WRITE"interrupts,<GET locative WRITE!-INTERRUPTS>likewise. - Otherwise, the
IHEADERisPUTon the nameATOMwith the indicatorINTERRUPT. Thus, for example,<GET CLOCK!-INTERRUPTS INTERRUPT>returns theIHEADERfor the clock interrupt or#FALSE ()if there is no association;<EVENT "CLOCK" 0>returns theIHEADER, creating it if there is no association.
<EVENT "MY-EVENT" 5>
<GET MY-EVENT!-INTERRUPTS INTERRUPT> ⇒ #IHEADER MY-EVENT!-INTERRUPTS
The interrupt system keeps its own tables in association space, using chapter
16's ordinary PUT and GET -- no private mechanism at all.
21.5.2. HANDLER
A HANDLER specifies a particular action for a particular interrupt. The
elements of a HANDLER are as follows:
- next
HANDLERif any, else a zero-lengthHANDLER - previous
HANDLERor theIHEADER(Thus theHANDLERs of a given interrupt form a "doubly-linked list" chaining between each other and back to theIHEADER.) - handler to be applied (anything
APPLICABLEthat evaluates its arguments -- the application is done not byAPPLYbut byRUNINT, which can take aPROCESSargument: see next line) PROCESSin which the handler will be applied, or#PROCESS 0, meaning whateverPROCESSwas running when the interrupt occurred (In the former case,RUNINTis applied to the handler and its arguments in the currently runningPROCESS, which causes anAPPLYin thePROCESSstored in theHANDLER, whichPROCESSmust beRESUMABLE. The runningPROCESSbecomesRESUMABLE, and the storedPROCESSbecomesRUNNING, but no otherPROCESSvariables (for exampleRESUMER) are changed.)
(The manual writes item 3's parenthesis as "anything but APPLICABLE", which
inverts its meaning; the handler must be APPLICABLE.)
21.6. Other SUBRs
<ON name applicable priority:fix process which>
is equivalent to
<HANDLER <EVENT name priority which>
applicable process>
ON is a combination of EVENT and HANDLER: it creates (or finds) the
IHEADER, associates and enables it, adds a HANDLER to the front of the list
(first to be performed), and returns the HANDLER.
<DISABLE iheader>
is effectively <PUT iheader 2 #LOSE -1>. Actually the TYPE LOSE is
unimportant, but the -1 signifies that iheader is disabled.
<ENABLE iheader>
is effectively <PUT iheader 2 #LOSE 0>. Actually the TYPE LOSE is
unimportant, but the 0 signifies that iheader is enabled.
<SETG FIRED <>> ⇒ #FALSE ()
<ON "MY-EVENT" <FUNCTION (X) <SETG FIRED .X>> 5>
<DISABLE <EVENT "MY-EVENT" 5>>
<INTERRUPT "MY-EVENT" 9> ⇒ #FALSE ()
,FIRED ⇒ #FALSE ()
<ENABLE <EVENT "MY-EVENT" 5>>
<INTERRUPT "MY-EVENT" 7> ⇒ T
,FIRED ⇒ 7
A disabled IHEADER makes INTERRUPT answer #FALSE (), which is the
difference from the OFFed-handler case in section 21.4 where it answered T.
Enabled-with-no-actions and disabled are distinguishable, and this is how.
21.7. Priorities and Interrupt Levels
At any given time there is a defined interrupt level. This is a FIX which
determines which interrupts can really "interrupt" -- that is, cause the current
processing to be suspended while their wants are satisfied. Normal,
non-interrupt programs operate at an interrupt level of 0 (zero). An interrupt is
processed at an interrupt level equal to the interrupt's priority.
21.7.1. Interrupt Processing
Interrupts "actually" only occur at well-defined points in time: during a call to
a Subroutine, or at critical places within Subroutines (for example, during each
iteration of MAPF on a LIST, which may be circular), or while a PROCESS is
"BLOCKED" (see below). No interrupts can occur during garbage collection.
What actually happens when an enabled interrupt occurs is that the priority of the interrupt is compared with the current interrupt level, and the following is done:
If the priority is greater than the current interrupt level, the current processing is "frozen in its tracks" and processing of the action(s) specified for that interrupt begins.
If the priority is less than or equal to the current interrupt level, the interrupt occurrence is queued -- that is, the fact that it occurred is saved away for processing when the interrupt level becomes low enough.
When the processing of an interrupt's actions is completed, MDL usually (1) "acts as if" the previously-existing interrupt level is restored, and processing continues on what was left off (perhaps for no time duration); and (2) "acts as if" any queued interrupt occurrences actually occurred right then, in their original order of occurrence.
21.7.2. INT-LEVEL
The SUBR INT-LEVEL is used to examine and change the current interrupt level
directly.
<INT-LEVEL>
simply returns the current interrupt level.
<INT-LEVEL fix>
changes the interrupt level to its argument and returns the previously-existing interrupt level.
<INT-LEVEL> ⇒ 0
<INT-LEVEL 5> ⇒ 0
<INT-LEVEL> ⇒ 5
<INT-LEVEL 0> ⇒ 5
<INT-LEVEL> ⇒ 0
Queueing, and the fact that a queued interrupt fires when the level drops:
<SETG FIRED <>> ⇒ #FALSE ()
<ON "MY-EVENT" <FUNCTION (X) <SETG FIRED .X>> 5>
<INT-LEVEL 9> ⇒ 0
<INTERRUPT "MY-EVENT" 11> ⇒ T
,FIRED ⇒ #FALSE ()
<INT-LEVEL 0> ⇒ 9
,FIRED ⇒ 11
Priority 5 against level 9 means "queued", so INTERRUPT answered T while
nothing ran; dropping the level to 0 delivered it. That is section 21.7.1's two
numbered rules, both of them, in six lines.
If INT-LEVEL lowers the priority of the interrupt level, it does not "really"
return until all queued occurrences of interrupts of higher priority than the
target priority have been processed.
Setting the INT-LEVEL extremely high (for example,
<INT-LEVEL <CHTYPE <MIN> FIX>>) effectively disables all interrupts (but
occurrences of enabled interrupts will still be queued).
If LISTEN or ERROR is called when the INT-LEVEL is not zero, then the
typeout will be
LISTENING-AT-LEVEL i PROCESS p INT-LEVEL i
21.7.3. DISMISS
DISMISS permits a handler to return an arbitrary value for an arbitrary
ACTIVATION at an arbitrary interrupt level. The call is as follows:
<DISMISS value:any activation int-level:fix>
where only the value is required. If activation is omitted, return is to the
place interrupted from, and value is ignored. If int-level is omitted, the
INT-LEVEL prior to the current interrupt is restored.
21.8. Specific Interrupts
Descriptions of the characteristics of particular "built-in" MDL interrupts
follow. Each is named by its STRING name. Expect this list to be incomplete
yesterday.
"CHAR" is currently the most complex built-in interrupt, because it serves duty
in several ways. All ways are concerned with characters or machine words that
arrive or depart at unpredictable times, because MDL is communicating with a
person or another processor. Each "CHAR" IHEADER has a CHANNEL for the
element that names the interrupt, and the mode of the CHANNEL tells what kinds
of "CHAR" interrupts occur to be handled through that IHEADER.
- If the
CHANNELis for input,"CHAR"occurs every time an "interesting" character (see below) is received from theCHANNEL's real terminal, or any character is received from theCHANNEL's pseudo-terminal, or a character or word is received from theCHANNEL's Network socket, or indeed (in the ITS version) the operating system generates an interrupt for any reason. - If the
CHANNELis for output to a pseudo-terminal or Network socket,"CHAR"occurs every time a character or word is wanted. - If the
CHANNELis for output to a terminal,"CHAR"occurs every time a line-feed character is output or (in the ITS version) the operating system generates a screen-full interrupt for the terminal.
21.8.1. "CHAR" received
A handler for an input "CHAR" interrupt on a real terminal must take two
arguments: the CHARACTER which was typed, and the CHANNEL on which it was
typed.
In the ITS version, the "interesting" characters are those "enabled for
interrupts" on a real terminal, namely ^@ through ^G, ^K through ^_, and
DEL (that is, ASCII codes 0-7, 13-37, and 177 octal).
In the Tenex and Tops-20 versions, the operating system can be told which
characters typed on a terminal should cause this interrupt to occur, by calling
the SUBR ACTIVATE-CHARS with a STRING argument containing those characters
(no more than six, all with ASCII codes less than 33 octal). If called with no
argument, ACTIVATE-CHARS returns a STRING containing the characters that
currently interrupt. Initially, only ^G, ^S, and ^O interrupt.
An initial MDL already has "CHAR" enabled on ,INCHAN with a priority 8
(eight), the SUBR QUITTER for a handler to run in #PROCESS 0 (the running
PROCESS); this is how ^G and ^S are processed. In addition, every time a
new CHANNEL is OPENed in "READ" mode to a terminal, a similar IHEADER and
HANDLER are associated with that new CHANNEL automatically. These
automatically-generated IHEADERs and HANDLERs use the standard machinery, and
they can be DISABLEd or OFFed at will. However, the IHEADER for
,INCHAN should not be OFFed: MDL knows that $ is typed only by an interrupt!
Example: the following causes the given message to be printed out whenever a ^Y
is typed on .INCHAN:
<SET H <HANDLER <GET .INCHAN INTERRUPT>
#FUNCTION ((CHAR CHAN)
#DECL ((VALUE) ANY (CHAR) CHARACTER (CHAN) CHANNEL)
<AND <==? .CHAR !\^Y>
<PRINC " [Some of my best friends are ^Ys.] ">>)>>$
#HANDLER #FUNCTION ((CHAR CHAN) ...)
<+ 2 ^Y [Some of my best friends are ^Ys.] 2>$
4
<OFF .H>$
#HANDLER #FUNCTION (...)
Note that occurrences of "CHAR" do not wait for the $ to be typed, and
the interrupting character is omitted from the input stream.
A "CHAR" interrupt can also be associated with an input CHANNEL open to a
Network socket ("NET" device). A handler gets applied to a NETSTATE array
(which see) and the CHANNEL.
In the ITS version, a "CHAR" interrupt can also be associated with an input
CHANNEL open to a pseudo-terminal ("STY" device and friends). An interrupt
occurs when a character is available for input. These interrupts are set up in
exactly the same way as real-terminal interrupts, except that a handler gets
applied to only one argument, the CHANNEL. Pseudo-terminals are not
available in the Tenex and Tops-20 versions.
For any other flavor of ITS channel interrupt, a handler gets applied to only
one argument, the CHANNEL.
pymdl's own.
"CHAR"is the interrupt most tied to a real terminal, and a pymdl session as often has a pipe. The mechanism is there and anIHEADERon a terminalCHANNELbehaves as described; what does not happen is the operating system deciding, on its own schedule, that a character has arrived. Chapter 14's box on terminalCHANNELs has the same distinction from the I/O side, and chapter 37 is where it matters most -- driving MDL 55 under the emulator means being the terminal, so the^Gand^Sof section 19's chapter are generated deliberately rather than typed.
21.8.2. "CHAR" wanted
A "CHAR" interrupt can be associated with an output CHANNEL open to a Network
socket ("NET" device). A handler gets applied to a NETSTATE array (which see)
and the CHANNEL.
In the ITS version, a "CHAR" interrupt can also be associated with an output
CHANNEL open to a pseudo-terminal ("STY" device and friends). An interrupt
occurs when the program at the other end needs a character (and the
operating-system buffer is empty). A handler gets applied to one argument, the
CHANNEL.
21.8.3. "CHAR" for new line
A handler for an output "CHAR" interrupt on a real terminal must take one or
two arguments (using "OPTIONAL" or "TUPLE"): if two arguments are supplied
by the interrupt system, they are the line number (FIX) and the CHANNEL,
respectively, and the interrupt is for a line-feed; if only one argument is
supplied (only in the ITS version), it is the CHANNEL, and the interrupt is for
a full terminal screen. Note: the supplied line number comes from the CHANNEL,
and it may not be accurate if the program alters it in subtle ways, for example,
via IMAGE calls or special control characters. (The program can compensate by
putting the proper line number into the CHANNEL.)
21.8.4. "GC"
"GC" occurs just after every garbage collection. Enabling this interrupt is
the only way a program can know that a garbage collection has occurred. A handler
for "GC" takes three arguments. The first is a FLOAT indicating the number of
seconds the garbage collection took. The second argument is a FIX indicating
the cause of the garbage collection, as follows (chapter 24):
- Program called
GC. - Movable storage was exhausted.
- Control stack overflowed.
- Top-level
LVALs overflowed. GVALvector overflowed.TYPEvector overflowed.- Immovable garbage-collected storage was exhausted.
- Internal stack overflowed.
- Both control and internal stacks overflowed (rare).
- Pure storage was exhausted.
- Second, exhaustive garbage collection occurred.
The third argument is an ATOM indicating what initiated the garbage collection:
GC-READ, BLOAT, GROW, LIST, VECTOR, SET, SETG, FREEZE, GC,
NEWTYPE, PURIFY, PURE-PAGE-LOADER (pure storage was exhausted), or
INTERRUPT-HANDLER (stack overflow, unfortunately).
<SETG SAW-GC <>> ⇒ #FALSE ()
<ON "GC" <FUNCTION ("TUPLE" T) <SETG SAW-GC <LENGTH .T>>> 3>
<GC>
,SAW-GC ⇒ 3
Three arguments, as documented, and the interrupt really does fire. Chapter 25
has what <GC> means here, which is not what it meant on a PDP-10.
21.8.5. "DIVERT-AGC"
"DIVERT-AGC" ("Automatic Garbage Collection") occurs just before a
deferrable garbage collection that is needed because of exhausted movable
garbage-collected storage. Enabling this interrupt is the only way a program can
know that a garbage collection is about to occur. A handler takes two arguments:
a FIX telling the number of machine words needed and an ATOM telling what
initiated the garbage collection (see above). If it wishes, a handler can try to
prevent a garbage collection by calling BLOAT with the FIX argument. If the
pending request for garbage-collected storage cannot then be satisfied, a garbage
collection occurs anyway. AGC-FLAG is SET to T while the handler is
running, so that new storage requests do not try to cause a garbage collection.
21.8.6. "CLOCK"
"CLOCK", when enabled, occurs every half second (the ITS "slow-clock" tick). It
is not available in the Tenex or Tops-20 versions. It wants handlers which take
no arguments. Example:
<ON "CLOCK" <FUNCTION () <PRINC "TICK ">> 1>
21.8.7. "BLOCKED"
"BLOCKED" occurs whenever any PROCESS (not only the PROCESS which may be
in a HANDLER) starts waiting for terminal input: that is, an occurrence
indicates that somewhere, somebody did a READ, READCHR, NEXTCHR, TYI,
etc. to a console. The handler for a "BLOCKED" interrupt should take one
argument, namely the PROCESS which started waiting (which will also be the
PROCESS in which the handler runs, if no specific one is in the HANDLER).
Example: the following will cause MDL to acquire a * prompting character.
<ON "BLOCKED" #FUNCTION ((IGNORE) <PRINC !\*>) 5>
21.8.8. "UNBLOCKED"
"UNBLOCKED" occurs whenever a $ (ESC) is typed on a terminal if a program was
hanging and waiting for input, or when a TYI call (which see) is satisfied. A
handler takes one argument: the CHANNEL via which the $ or character is
input.
21.8.9. "READ" and "WRITE"
"READ" and "WRITE" are associated with read or write references to MDL
objects. These interrupts are often called "monitors", and enabling the interrupt
is often called "monitoring" the associated object. A "read reference" to an
ATOM's local value includes applying BOUND? or ASSIGNED? to the ATOM;
similarly for a global value and GASSIGNED?. If the INT-LEVEL is too high
when "READ" or "WRITE" occurs, an error occurs, because occurrences of these
interrupts cannot be queued.
Monitors are set up with EVENT or ON, using a locative to the object being
monitored as the extra which argument, just as a CHANNEL is given for "CHAR".
A handler for "READ" takes two arguments: the locative and the FRAME of the
function application that makes the reference. A handler for "WRITE" takes
three arguments: the locative, the new value, and the FRAME. For example:
<SET A (1 2 3)> ⇒ (1 2 3)
<SET B <AT .A 2>> ⇒ #LOCL 2
<SETG CHANGED <>> ⇒ #FALSE ()
<ON "WRITE" <FUNCTION (OBJ VAL FRM) <SETG CHANGED .VAL>> 4 0 .B>
<1 .A 10> ⇒ (10 2 3)
,CHANGED ⇒ #FALSE ()
<2 .A 20> ⇒ (10 20 3)
,CHANGED ⇒ 20
<OFF "WRITE" .B> ⇒ #IHEADER #LOCL 20
<SETG CHANGED <>> ⇒ #FALSE ()
<2 .A 30> ⇒ (10 30 3)
,CHANGED ⇒ #FALSE ()
The <1 .A 10> line is the monitor being specific: the locative points at
element 2, so writing element 1 is invisible to it. The manual's version of this
example prints a sentence instead, ending
Program changed #LOCL 2 to 20 via #FRAME PUT. Note that the IHEADER prints
as #IHEADER #LOCL 20 at the end and the locative printed as #LOCL 2 at the
start: a locative prints the value it currently points at, and by then the
element really is 20. The manual's transcript ends the same way.
Found along the way. The third argument is a
FRAMEbyTYPEand prints as#FRAME PUT, so the manual's example -- which onlyPRINCs it -- works exactly as printed. But it is a stand-in, not a real stack frame, so a handler that does more than print it finds out:<FUNCT .FRM>answersFIRST-ARG-WRONG-TYPErather than theATOMPUT, andARGSlikewise. That is a live gap rather than a deliberate departure, recorded here because this chapter is where a reader would meet it; appendix E carries it with the other measured differences. Everything else in the section holds, including the specificity above andOFFby name and locative.
21.8.10. "SYSDOWN"
"SYSDOWN" occurs when a system-going-down or system-revived signal is received
from ITS. It is not available in the Tenex or Tops-20 versions. If no IHEADER
is associated and enabled, a warning message is printed on the terminal. A
handler takes one argument: a FIX giving the number of thirtieths of a second
until the shutdown (-1 for a reprieve).
21.8.11. "ERROR"
In an effort to simplify error handling by programs, MDL has a facility allowing
errors to be handled like interrupts. SETGing ERROR to a user function is a
distasteful method, not safe if any bugs are around. An "ERROR" interrupt wants
a handler that takes any number of arguments, via "TUPLE". When an error
occurs, handlers are applied to the FRAME of the ERROR call and the TUPLE of
ERROR arguments. If a given handler "takes care of the error", it can ERRET
with a value from the ERROR FRAME, after having done <INT-LEVEL 0>. If no
handler takes care of the error, it falls into the normal ERROR.
If an error occurs at an INT-LEVEL greater than or equal to that of the
"ERROR" interrupt, real ERROR will be called, because "ERROR" interrupts
cannot be queued.
<SETG SAW-ERROR <>> ⇒ #FALSE ()
<ON "ERROR" <FUNCTION ("TUPLE" T) <SETG SAW-ERROR <LENGTH .T>>> 4>
<+ A 1> ⇒ *ERROR* ARG-WRONG-TYPE
,SAW-ERROR ⇒ 3
The handler ran, saw three arguments, declined to take care of the error, and the error then happened normally -- which is the documented fall-through.
pymdl's own.
"ERROR"is the most useful interrupt in this chapter for anything that drives MDL from outside it, because it converts a listener dialogue into a function call. It is whattools/mdl55_errhandler.pyinstalls on the era side to ask MDL 55 seven hundred questions without restarting it (chapter 37), and it is the right tool for a program that must survive its own bugs.
21.8.12. "IPC"
"IPC" occurs when a message is received on the ITS IPC device (chapter 25). It
is not available in the Tenex and Tops-20 versions.
pymdl's own.
<IPC-ON>opens a Unix domain socket for a pymdl identity. Received messages are delivered through the interrupt queue at a polling point; MDL handlers run on the interpreter thread. Seesrc/pymdl/subrs/ipc.pyandtests/test_mudinq.py.
21.8.13. "INFERIOR"
"INFERIOR" occurs when an inferior ITS process interrupts the MDL process. It
is not available in the Tenex and Tops-20 versions. A handler takes one argument:
a FIX between 0 and 7 inclusive, telling which inferior process is
interrupting.
21.8.14. "RUNT" and "REALT"
These are not available in the Tenex and Tops-20 versions.
"RUNT", if enabled, occurs once, N seconds of MDL running time (CPU time)
after calling <RUNTIMER N:fix-or-float>, which returns its argument. A handler
takes no arguments. If RUNTIMER is called with no argument, it returns a FIX,
the number of run-time seconds left until the interrupt occurs, or #FALSE () if
the interrupt is not going to occur.
"REALT", if enabled, occurs every N seconds of real-world time after calling
<REALTIMER N:fix-or-float>, which returns its argument. A handler takes no
arguments. <REALTIMER 0> tells the operating system not to generate real-time
interrupts. If REALTIMER is called with no argument, it returns a FIX, the
number of real-time seconds given in the most recent call to REALTIMER with an
argument, or #FALSE () if REALTIMER has not been called.
<RUNTIMER> ⇒ #FALSE ()
<REALTIMER> ⇒ #FALSE ()
Both answer #FALSE () in a fresh interpreter, which is the documented "has not
been called" answer.
21.8.15. "Dangerous" Interrupts
"MPV" ("memory protection violation") occurs if MDL tries to refer to a storage
address not in its address space. "PURE" occurs if MDL tries to alter read-only
storage. "ILOPR" occurs if MDL executes an illegal instruction ("operator").
"PARITY" occurs if the CPU detects a parity error in MDL's address space. All
of these require a handler that takes one argument: the address (TYPE WORD)
following the instruction that was being executed at the time.
"IOC" occurs if MDL tries to deal illegally with an I/O channel. A handler must
take two arguments: a three-element FALSE like one that OPEN might return, and
the CHANNEL that got the error.
Ideally these interrupts should never occur. In fact, in the Tenex and Tops-20 versions, these interrupts always go to the superior operating system process instead of to MDL. In the ITS version, if and when a "dangerous" interrupt does occur:
- If no
IHEADERis associated with the interrupt, then the interrupt goes to the superior operating system process. - If an
IHEADERis associated but disabled, the errorDANGEROUS-INTERRUPT-NOT-HANDLEDoccurs (FILE-SYSTEM-ERRORfor"IOC"). - If an
IHEADERis associated and enabled, but theINT-LEVELis too high, the errorATTEMPT-TO-DEFER-UNDEFERABLE-INTERRUPToccurs.
pymdl's own. These five describe a KA-10 or KL-10 faulting, and there is no such machine under this interpreter -- pymdl's own errors arrive as chapter 19's
ERROR, not as hardware traps. But they are not purely historical either: chapter 36 does run PDP-10 instructions, and an illegal one there is a real event the emulator must decide what to do about. This section is the specification it was written against.
21.9. User-Defined Interrupts
If the interrupt name given to EVENT or ON is not one of the standard
predefined interrupts of MDL, they will gleefully create an ATOM in
<INTERRUPTS> and an associated IHEADER anyway, making the assumption that you
are setting up a "program-defined" interrupt.
Program-defined interrupts are made to occur by applying the SUBR INTERRUPT,
as in
<INTERRUPT name arg1 ... argN>
where name is a STRING, ATOM or IHEADER, and arg1 through argN are the
arguments wanted by the handlers for the interrupt.
If the interrupt specified by INTERRUPT is enabled, INTERRUPT returns T;
otherwise it returns #FALSE (). All the usual priority and queueing rules hold,
so that even if INTERRUPT returns T, it is possible that nothing "really
happened" (yet).
INTERRUPT can also be used to cause "artificial" occurrences of standard
predefined MDL interrupts.
Making a program-defined interrupt occur is similar to calling a handler directly,
but there are differences. The value returned by a handler is ignored, so side
effects must be used in order to communicate information back to the caller, other
than whether any handler ran or will run. One good use for a program-defined
interrupt is to use the priority and queueing machinery of INT-LEVEL to control
the execution of functions that must not run concurrently. For example, if a
"CHAR" handler just deposits characters in a buffer, then a function to process
the buffered characters should probably run at a higher priority level -- to
prevent unpredictable changes to the buffer during the processing -- and it is
natural to invoke the processing with INTERRUPT.
In more exotic applications, INTERRUPT can signal a condition to be handled by an
unknown number of independent and "nameless" functions. The functions are
"nameless" because the caller doesn't know their name, only the name of the
interrupt. This programming style is modular and event-driven, and it is one way
of implementing "heuristic" algorithms. In addition, each HANDLER has a
PROCESS in which to run its handler, and so the different handlers for a given
condition can do their thing in different environments quite easily, with less
explicit control than when using RESUME.
pymdl's own. Everything in this section works, and it is the half of the chapter that depends on no operating system at all: a program-defined interrupt is a name, a priority, a queue and a list of handlers, and every example in this chapter is built on one. "Modular and event-driven" in 1979 is worth pausing on -- this is a publish-and-subscribe system with priorities, described as an obvious consequence of the interrupt machinery.
21.10. Waiting for Interrupts
21.10.1. HANG
<HANG pred>
hangs interruptibly, without consuming any CPU time, potentially forever. HANG
is nice for a program that cannot do anything until an interrupt occurs. If the
optional pred is given, it is evaluated every time an interrupt occurs and is
dismissed back into the HANG; if the result of evaluation is not FALSE, HANG
unhangs and returns it as a value. If pred is not given, there had better be a
named ACTIVATION somewhere to which a handler can return.
21.10.2. SLEEP
<SLEEP time:fix-or-float pred>
suspends execution, interruptibly, without consuming any CPU time, for time
seconds, where time is non-negative, and then returns T. pred is the same as
for HANG.
<SLEEP 0> ⇒ T
<HANG <FUNCTION () T>> ⇒ T
Both take the escape immediately, which is the only way to show them in a book whose examples must terminate.