Comput Lang. VoL 9. No. 3 4. pp. 18:~192. 1984
0096-0551 8-' 5 3 0 0 - 0 0 0 Cop)right , 1984 Pergamon Press ktd
Pnnted in Great Britain. All fights reserved
REVERSE
EXECUTION
IN A GENERALIZED
CONTROL
REGIME
FERNANDO LAFORA a n d M A R Y L O U SOFFA Computer Science Department, University of Pittsburgh, Pittsburgh, PA 15260. U.S.A.
(Receiced 27 September 1983) Abstract--The value of advanced control forms in programming languages is currently being recognized. A combination of control forms in a single environment can increase the expressive power of a s~stem but may lead to difficulties in semantic definition and implementation. This paper describes an implementation for a control combination of backtracking and logical concurrent structures. In particular, we present a reversible execution implementation for backtracking across scheduled coroutines or processes as defined in Simula [I]. Such a control combination, particularly useful for debugging simulation programs, is incorporated as a basic feature in a debugging tool expecially designed (or programs using scheduled processes. Backtracking
Reverse execution
Debugging
I.
Simula-67
Simulation
Processes
INTRODUCTION
The expressive power of a high level programming language increases with the inclusion of higher forms of control such as concurrency, scheduled processes and backtracking. Currently the importance of the design and combination of higher control forms in high level languages is being recognized as indicated by the development of numerous languages possessing various forms of concurrency. However, even with the evolution to more natural control forms, programming errors continue to be a problem and because of the lack of both "error tolerant software" and simple methods for proving the correctness of programs, debugging of programs possessing these higher forms of control is a necessary and time cOnsuming activity of programming. As the expressive power of programming languages increases with the introduction of higher forms of control, so may debugging languages. In fact, perhaps more expressive control forms are needed to debug programs using complex control forms. Programs written in simulation languages, usually containing some form of high level control to allow for the simulation of concurrent events, are notoriously difficult to debug due in part to the complexity of the programs and the long execution times. With the long execution time, it is impractical to stop and restart a program in order to isolate the region of the program where an error occurred. The complicated execution state also makes the error detection more difficult. This paper addresses the issue of control extension in debugging languages; that is, we consider the incorporation of a high level control form into debugging tools for languages possessing higher level control structures. In particular, the control combination of backtracking over scheduled coroutines is considered. A design of a state-saving technique for implementing backtracking over a block-structured control regime is described in [2]. However, in a debugging setting, reversible execution is the more valuable form of backtracking as the execution of sequential statements can be undone, one after another, allowing an incremental examination of the execution state without previously tagging execution points. As simulation languages are difficult to debug, we consider the development of a backtracking scheme that can be used in a programming language such as Simula with language-defined simulation operations. In a debugging environment, this control form would allow the user to debug programs by "unrolling" the execution in order to locate the region when the error occurred and thus eliminate re-execution of the entire program. The initial work of reversible execution was done by Floyd [3] who devised an algorithm for converting non-deterministic algorithms containing Fortran-type statements such as assignment, conditional, go to and subprogram calls into deterministic algorithms. Cohen [4] extended the work to include the case and while statements. 183
I84
FER.~'ANDO LAFO~A and MARY LOU SOFFA
In this work, we implement a reversible execution scheme in a simulation environment which contains the concept of recursion and logical concurrency. Unlike the previous work, the reversible execution must also consider storage management details such as the allocation and deallocation of storage as well as the inverse of each control form in the language. Coupling this with the previous work, a debugging tool that includes reversible execution has been written for debugging a language that utilizes the simulation control primitives of Simula. The paper is organized by presenting a brief description of the control simulation primitives of Simula in Section 2. Section 3 discusses the problems involved in reversing the actions of these primitives and then develops an implementation for reversing the execution of each simulation control primitive. The technique is demonstrated in Section 4 by an example. Storage management details are considered in Section 5. 2. S I M U L A T I O N F E A T U R E S IN S I M U L A System defined simulation features in Simula are provided by prefixing a block with the predefined class called S I M U L A T I O N . As this work centers around the combination of backtracking and higher forms of control structures, the control operations involved with process manipulation are our primary concern. The brief discussion of the semantics of the simulation primitives given is restricted to information and notation needed in order to develop reversible execution for processes. 2.1 The process class
Scheduling of a process program module in Simula is clone using a system defined clock. Thus, implicit in the semantics of the primitives manipulating processes is the concept of a time chain or event list (L) in which events are scheduled to occur, based on time. At any given time, the process at the head of the list is the active process (CURRENT) whose actions are taking place at the current system time. Processes that are scheduled but not active are said to be suspended. Other processes in existence that are not on the event list are either in a passive or terminated state. Simulation primitives that manipulate processes do such actions as insert a process in L, remove a scheduled process from L and activate a process. Times of suspended processes can also be changed. The system operates by activating the process at the head of L and executing the associated code until an operation is encountered which transmits control directly or indirectly to another process through insertion of the process at the head of L. Process instances are created by means of the primitive N E W that allocates and initializes space for the activation record (AR) of the process. Because N E W does not start executing the process, its actions do not interfere with L and therefore need not be discussed further. It is understood then, that when a process occurs for the first time in a scheduling primitive (this has to be ACTIVATE or REACTIVATE), its AR has already been created. 2.2 Process operations In the following discussion, X and Y refer to process instances. Tis a real expression representing time, CURRENT is the current process and T I M E is the system time. We assume the correct application of the primitives as we are only concerned with undoing control operations permitted by the language. There are three different forms which can be used to activate a process or schedule a process for activation. They include an immediate activation, a time-based activation and an event-based activation. If the argument of an activation operation is already active, the operation is equivalent to a no operation (NOP).
(a) (b)
(RE)ACTIVATE X: The effect of this immediate activation is to set X to CURRENT with a time equal to the current system time, TIME. (RE)ACTIVATE X A T / D E L A Y T[PRIOR]: There are numerous options for this time-based operation. If D E L A Y is used, X is inserted in L after the last element whose associated time is equal to or less than T I M E + T. The new time associated with X is T I M E + T. If AT is
Reverse execution
185
used, X is placed in L after the last element whose associated time is equal to or less than T. The new time associated with X is T. (RE)ACTIVATE X A F T E R / B E F O R E Y: The effect of this event-based activation is to put X after before Y in L. The new time of X is set equal to that of Y. If either Y is passive or X equals Y, this activation statement is equivalent to CANCEL(X) (see below).
(c)
The execution of an active or scheduled process can be delayed by the following four primitives: (d)
(e)
(e) (f)
HOLD(T): The effect is to reschedule CURRENT in L after the last scheduled process with a time less than or equal to T. The new time of this process becomes TIME + T. If T is less than 0, the activation statement is equivalent to HOLD(0). Note that if the process following CURRENT has a scheduled time greater than T I M E + 7", the only effect of H O L D ( T ) is to increment the time of CURRENT by T. CANCEL(X): This primitive removes X from L. If X is passive or X equals N O N E then the operation is equivalent to a NOP. If X is the CURRENT process, the second element in L becomes active; otherwise the CURRENT process continues execution. PASSIVATE: This operation is essentially a CANCEL(CURRENT). WAIT(Q): CURRENT is removed from L and inserted in a user-define list, Q.
We now consider the techniques to reverse the actions of the above operations. 3. B A C K T R A C K I N G OF M O D U L E C O M M U N I C A T I O N In the previous work on backtracking using a reverse execution viewpoint, a "memory" stack is constructed when execution progresses in the forward direction in order to save the necessary information to enable backtracking. Thus, for example, when executing an assignment statement in the forward direction, a copy of the original value of a variable is stored in the stack before the new assignment is made. In the reverse direction, the variable receives the original value by popping the value from the stack. For backtracking across control structures that express module communication, the process is more complex due to the environment that must be restored. We first consider recursive procedure calls and returns.
3.1 Backtracking of procedures The typical execution state when modules are communicating by procedure calls and returns consists of a run-time stack of activation records (ARs) and a pointer to the currently executing procedure's activation record. Assume in the program skeleton of Fig. 1 that MAIN, P and Q are program modules and M~, M 2, P~ and P~_ represent blocks of code. During execution in the forward direction, assume the statements executed are M~, Pt, Q, P2 and Me in that order. If we want to backtrack from a point after M2 as indicated by an *, we have to undo the execution of M,_, P2, Q, P~ and M t. During reversible execution, when CALL P is encountered after backtracking M.,, the objective is to execute P in the reverse direction and then return control to label a' in MAIN. This is similar to a call in the forward direction and the actions to be performed when bactracking follow: (1) put in the AR for MAIN the continuation point which is a', (2) push onto the run-time stack an AR for P, and (3) begin to execute P backwards at the return statement that sent control to MAIN in the forward direction. When the execution of P is totally reversed, i.e. when its BEGIN is reached, the AR for P is removed and MAIN continues at a', (of course, executing P implies executing Q). The above technique to accomplish this reverse action, suggested by Floyd [3] for a non-recursive environment, is similar to executing a procedure call in the forward direction. Thus, the inverse of a CALL to a procedure is a CALL to the same procedure. There is, however, a complication due to state saving in a recursive environment. Under normal conditions, i.e. when no backtracking is expected, the AR of the procedure is discarded upon termination. However, in a backtracking
186
FERNANDO LAFOP.-', and MARY Lot." SOFFA
PROCEDURE MAIN;
a':
CALL P a:
PROCEDURE P;
b':
CALL Q; b:
END; PROCEDURE Q:
}°1
END: Fig. 1. Procedure calls.
mode, when a call P is executed, we need an A R for P that is structured exactly as it was when return was reached in the forward direction. The reason for this is that, as we are in a debugging environment, the user may want to interrogate the execution state at the return statement of P. We also need to set the program counter to the correct return point, in case there is more than one return statement in the module. Thus, the AR needs to be saved or reconfigured for backtracking. The storage management problem for recursive procedures is discussed in Section 4 along with the storage management problem for backtracking processes. 3.2 Backtracking of simulation primitives Due to the L I F O discipline of the procedure call/return, when a CALL P is encountered while backtracking, it is clear that P was the last module active and thus P must again be made active in reverse execution. The situation is more complex when considering logical concurrency. For example, suppose an A C T I V A T E ( Q ) is encountered in the reverse direction. It is not true that Q was the last module active in the forward direction before control was passed to the present module. Thus, we have to identify the module and the instance of the module to which control has to be passed. It is also not true that the inverse operation is the same operation, i.e. ACTIVATE, as was the case for procedure calls. Thus, we have to specify the operation that has to be performed to reverse the execution state. In a control regime more general than recursion, backtracking is also more difficult in that the execution state which has to be restored is more complex. In the execution state for a program employing the simulation control structures of Simula, activation records for the processes must be restored as well as the linkage between them and the times they execute or are scheduled to execute. Thus the structure of L must be reconfigured when backtracking which involves the resetting of time. In the next section, inverse operations and information needed to reconfigure the execution state are determined. The management of storage, including the problem of garbage collection in a reverse execution scheme is discussed in Section 4. 3.2.1 Reverse operations. During execution, L changes its state from an initial state So to S~ to $ 2 . . . to 5",. A problem, then, is to determine the backward state transition from state 5', to S,_ ~ . . . to S~. To do this, each time we pass from state Si to 5",.+~, we push onto the memory stack M enough information that enables us to subsequently pass from St+ ~ back to Sg. The problem then is to specify:
Reverse execution
187
(1) when do the states change from S,. t to S,; (2) what information is needed in the memory stack. For the first problem, consider the following program segment in some process class A: PASSIVATE: a:=
2:
and let X reference an instance of this class. After executing P A S S I V A T E during forward execution, X is no longer C U R R E N T , and another process becomes C U R R E N T . If the statement following the PASSIVATE, i.e. a : = 2, is subsequently executed it is due to a control operation being executed that makes X again C U R R E N T . Statements following a control operation are executed when some statement returns control to the process, i.e. when L changes its state. Accordingly, encountering a control operation upon backtracking implies that at this program point there was a change of state in the forward execution, and the information about the state has to be popped off the memory stack in order to return to the previous state. To determine the information needed to be saved in the stack, the effect of the different primitives on the execution state, and what actions have to be taken to undo the effect must be considered. The effects of the primitives as well as the actions needed to reverse the effects follow. The inverse actions are summarized in Table I. 3.2.2 A C T I V A T E X. The effect on the execution state of ACTIVATE(X) (at time or position) are: forward effect: (1) If X is passive--put X in L at the indicated place or time. (2) If X is not passive--no operation (NOP). In order to reverse the effects of an A C T I V A T E based on case (1), it is necessary to remove X from L. We can use the CANCEL(X) operation to do this. Thus in the forward direction, we must stack a pointer to the activation record for X and the operation CANCEL. For case (2), since no action was performed in the forward direction, no action has to be performed in the reverse direction. We thus need to stack a NOP primitive to indicate this. Corresponding to the two cases, we summarized: reverse action: (1) remove X from L, i.e. CANCEL X. (2) NOP. 3.2.3 REACTII/A TE. The effect of REACTIVATE(X) (time or place) on the execution state is: forward effect: whether X is passive or suspended, insert X in L at the indicated time and place.
Table 1. Inverse operations Stack information needed given status of instance X Scheduling primitive CANCEL(X)
Passivated NOP
Not passivated REACTIVATE* X PRED(X) X.TIME REACTIVATE* CURRENT NONE CURRENT.TIME
HOLD,WAIT PASSIVATE ACTIVATE(X)
CANCEL X
REACTIVATE (X)
CANCEL
NOP REACTIVATE" X PRED(X) X.TIME
188
FERNANDO LAFORAand MARY Lou SOFFA
If X was passive before the execution of REACTIVATE in the forward direction, then, to undo the effect of REACTIVATE, we must make it passive again. Therefore, a CANCEL(X) must be performed which necessitates stacking X and the CANCEL operation. If X was originally scheduled elsewhere on L, say after Y, with a certain time T, we must restore X to its previous place in L and time. To do this, we need to stack (i) a pointer to X's AR, (ii) information about the location of X before the REACTIVATE and (iii) its time T. The location can be indicated by saving the pointer to Y, the predecessor of X in the list. NONE is stacked in case X has no predecessor, i.e. it is tt~.e C U R R E N T process. To undo the effect of REACTIVATE, the action that has to be performed is a REACTIVATE X at the appropriate place and time. However, the Simula REACTIVATE cannot be used because of the possible decreasing time. Also, Simula only allows the specification of either a place or a time parameter and in this case, we need both. A new primitive is needed that permits the specification of: (1) a process X to be reactivated, (2) a process Y after which X has to be inserted, and (3) a time for X that can be less than the current time. This primitive will be referred to as REACTIVATE*. We have then, reverse action: If X was not originally in L, CANCEL(X); if X was in L put it back in its original place with its previous time, by using REACTIVATE*. 3.2.4 HOLD. The action of H O L D (time) is defined as: forward effect: schedule CURRENT (call it X) elsewhere in L with the indicated time. To reverse a H O L D , we have to move the AR of the X to the front of L and thus make it current and reset the time. The REACTIVATE* operation, a pointer to X, N O N E (to indicate that the process to be reactivated had no predecessor), and the current time are all needed. Therefore, reverse action: remove X from its current position and insert it at the head of L with time T. 3.2.5 PASSIVA TE. Suspension of the currently executing process by a PASSIVATE is done by: forward effect: remove CURRENT with time T from L. We use X to denote this instance. To undo the PASSIVATE, we have to put X at the head of L with the appropriate time. As with the H O L D , we stack the REACTIVATE* operation, a pointer to X, N O N E (to indicate that the process to be reactivated had no predecessor), and the current time. Thus, reverse action: put X at the head of L with a time equal to T. 3.2.6 WAIT. The forward and reverse actions are the same as for the PASSIVATE. 3.2.7 CANCEL. The execution state changes on CANCEL(X) by: forward effect: (I) If X is passive, a NOP is essentially executed. (2) If X is not passive, remove process X with time T from L. In case (1), only a NOP has to be stacked. To reverse the action of case (2), we have to replace X in the same position it was in L before the CANCEL operation. Obviously, this requires again a REACTIVATE* operation with its corresponding parameters. So, reverse action: (l) NOP (2) put process X where it was in L with time equal to T.
Reverse execution
189
3.3 Summary of reversing the operations We see that in Table 1. according to the primitive executed, only three inverse operations are necessary to restore the execution state: (I) NOP (2) CANCEL a process (3) restore a process to its previous place and time in L by using a new primitive REACTIVATE* Accordingly, the information needed in the stack is an indication of what action is to be performed, together with the necessary parameters• These parameters are the following: for NOP, no parameters; for CANCEL, the process to be cancelled; for REACTIVATE*, the process to be put in L, the predecessor of the process, and the time T. It should be noted that it is necessary to stack the inverse operation of an operation as it is not possible to determine the inverse during backtracking by knowing only the original operation. The inverse is dependent on the status of the module instance during forward execution as well as the operation. MAIN BLOCK; %
X:- NEW A;
tM1
ACTIVATE X;
} M2
HOLD (100); PROCESS CLASS A; %
Y:- NEW B;
tA1
ACTIVATE Y AFTER CURRENT; HOLD (25):
tA3
• . .
PROCESS CLASS B; %
Z:- NEW C;
tB1
ACTIVATE Z BEFORE X;
PASSlVATE;
PROCESS CLASS C; C1 REACTIVATE AT 30;
CANCEL (X):
Fig. 2. Simula program skeleton.
tc2 tc3
190
FER.";ANDO LAFORA a n d MARY LOU SOFFA
4. AN E X A M P L E Consider the program in Fig. 2 consisting of a MAIN program and three processes named A, B and C where blocks of code are represented by Ai, B~ and C,. Table 2 is a summary of the events occurring when the example program executes first in the forward direction, until C 3, and then in the backward direction. The superscripts with the processes in the first column indicate the scheduled time of the process. In the forward direction, the first column represents the status of L (processes and their respective times) when the statements in the second column are being executed. Column three shows the information pushed onto the memory stack when a scheduling primitive is found (at the end of the code block). For instance, row 5 indicates that when executing statements B,, i.e. actions of the class B between ACTIVATE Z B E F O R E X and PASSIVATE, L has four elements, Y, M, Z and X with times 0, 0, 25 and 25 respectively. When PASSIVATE is found in class B the following is pushed onto the stack: REACTIVATE* Y NONE 0 As a consequence, the new state is as shown in row 6, i.e. there are three processes M, Z and X with times 0, 25 and 25 respectively, and M is executing the set of statements M2 between ACTIVATE X and HOLD(100). When reversing the execution, information in the third column is used to change the state. Column three of the previous row gives the information popped off the stack when a scheduling primitive is found. For example, if we are executing statements M z in the backward direction, (see Table 2, row 6), L consists of process M, Z and X. When the primitive ACTIVATE X is found in the main program we pop off from the stack the information in row 5, column 3, that is: REACTIVATE* Y NONE 0 Table 2. Time chain and information stacked to reverse execution of sample program Statements being executed
Push onto/pop from memory stack
(I) M °
M~
CANCEL X
(2) X ° ~ M °
A~
CANCEL X
A2
REACTIVATE*
Process queue
(3)
X °~
yO
M o
X NONE 0 (4) yO~ MO~ X25
At
CANCEL X
(5) y0 ~ MO~ Z - ' ~ X-'~
B:
REACTIVATE Y NONE 0
(6) M ° ~ Z 25~ X 's
M:
REACTIVATE*
M NONE 0
(7)
Z2S ~
xZS ~
M ~°°
(8)
Z z~ ~
X 25 ~
y~O ~
M ioo
C~
CANCEL Y
C,.
REACTIVATE* X Z 0
(9) Z : 5 ~ y~O~ M t ~
C~
Re,,erse execution
19 [
and therefore put Y at the head of L with a time equal to O. L, then, becomes as indicated in row 5, column 1.
5. S T O R A G E M A N A G E M E N T
ISSUES
As indicated, for an implementation of a high level language using a static storage management scheme, the reverse actions as given above would be all that are needed to reverse the control operations. However, most high level languages currently in use require a dynamic storage management scheme. Thus, for the backtracking scheme to work, the actions taken by the storage management scheme must also be undone. For a recursive regime, whenever a procedure terminates, the storage for its activation record is reclaimed and reused. In a retentive control regime containing such control structures as coroutines, processes or concurrent tasks, storage can be reclaimed only when there are no references to it. A reference counting scheme and/or a garbage collector is used to reclaim storage. Simula typically uses a garbage collection scheme. For example, consider an assignment of the type X:-Y in Simula. where X and Y are both reference variables to processes. Before the assignment is made, assume that X references some process G which has no other references. After the assignment, the storage used by G can be reclaimed by the garbage collector. According to the reversible execution scheme developed above, a pointer to G would be stored in the memory stack. However, when this statement is reversed, the activation record associated with G is no longer available. The same situation holds for procedure or process terminations. In order to avoid the problem of the activation record being reclaimed, the activation record must be saved on the memory stack. When a module terminates, the contents and location of its AR are saved in the memory stack. Upon encountering a call to a procedure in the reverse direction, the first action is to restore the AR in the run-time storage from the memory stack at the location indicated. If a garbage collector is used during execution of a program, it is necessary to copy onto the memory stack all those activation records that are marked as free and thus collected. (Note, as we are only concerned in this paper with reversing control operations, we do not address dynamic variable storage deallocation.) Besides copying the A R onto the stack, the address in memory where the AR resides as well as a tag indicating that this was garbage collected also has to be stacked. The garbage collector is usually called upon whenever a statement is executed that requires allocation of storage. In Simula, the statements requiring storage allocation are module creation such as NEW, procedure/block entry calls and, depending on the implementation, perhaps (RE)ACTIVATE. Thus, in reversing either of these statements, a check has to be performed to determine if the top of the memory stack contains garbage. If so, the contents are copied into run-time storage at the location indicated. It should be pointed out that copying into the previously held location in memory is valid and, indeed, necessary in order to avoid complications. First of all, if the storage had been reused, by the time the place where the garbage was collected is reached when executing in reverse, the storage should be free (as it was in the forward direction). Also, as the A R has pointers to other ARs such as access links, control links and reference variables, the links must again be reestablished correctly. By copying the AR into the exact location where it previously resided, this problem of resolving pointer references is eliminated. If a reference counting scheme is used, the same general technique as described can be used. Copying an AR from the run-time storage to the memory stack at garbage collection time seems to invalidate the usefulness of storage reclamation; the garbage collector is called because of lack of available storage and copying it to another place in memory does not free any memory. Part of or all of the entire memory stack can be stored in secondary storage, for when backtracking starts, only the most recently stored items are processed first. The stack is thus contracted and used in a strict LIFO order. Obviously having the entire stack on secondary storage would cost in execution time. Studies are needed to gain insight into efficiency issues such as the size in memory that should be allocated for the memory stack.
192
FERNANDO LAFORAand MARY LO~.:SOFFA 6. C O N C L U D I N G
REMARKS
In this work, we have extended the n o t i o n o f b a c k t r a c k i n g to include b a c k t r a c k i n g over the logical c o n c u r r e n t c o n t r o l form as defined by the concept o f process found in Simula. A n i m p l e m e n t a t i o n is presented which uses reversible execution to i m p l e m e n t the b a c k t r a c k i n g . The b a c k t r a c k i n g scheme as presented has been i m p l e m e n t e d as part o f a d e b u g g i n g tool possessing high level c o n t r o l structures. The scheme can be s u m m a r i z e d as follows: the original code o f a p r o g r a m is modified to include o p e r a t i o n s to push i n f o r m a t i o n o n t o a m e m o r y stack during f o r w a r d execution. Also, b a c k w a r d code is c r e a t e d that will u n d o the f o r w a r d statements. In o r d e r to d o this, the b a c k w a r d code utilizes the i n f o r m a t i o n in the m e m o r y stack. The saving o f b a c k t r a c k i n g i n f o r m a t i o n can be stated at a n y d e s i g n a t e d p o i n t in a p r o g r a m . Experience is being gained by using this tool, and the i m p l e m e n t a t i o n is c u r r e n t l y being analyzed for space a n d time requirements. The b a c k t r a c k i n g c o n t r o l form is also used as a basis for investigating other forms o f higher level c o n t r o l structures that are useful in a d e b u g g i n g setting [5]. In particular, a d e b u g g i n g language is d e v e l o p e d that could be used in a retentive c o n t r o l regime and that allows the user to m o n i t o r the p r o g r a m in a b a c k w a r d direction to u n d o the effect o f previously executed statements. Also, by means o f reverse execution, a p r o g r a m can look a h e a d from a b r e a k p o i n t to see if some future event h a p p e n s or some c o n d i t i o n holds. The p r o g r a m view is always from the b r e a k p o i n t . The system informs the user if the event will h a p p e n or the c o n d i t i o n will hold. O f course, a default m a x i m u m n u m b e r o f s t a t e m e n t s to l o o k a h e a d has to be set up in case the event or c o n d i t i o n never happens. In this way, future b e h a v i o r o f the p r o g r a m can be predicted w i t h o u t logically executing it. Acknowledgements--This work was supported in part by grant MCS-79-06102 from the National Science Foundation to
the University of Pittsburgh. REFERENCES 1. Dahl O.-J., Myhrhaug B. and Nygaard K., Simulation 67 Common Base Language. Publication No. S-22, Norwegian Computing Center, Oslo (1970). 2. Sampson A. W., A model and implementation of backtracking in a generalized control setting. M.S. Thesis, Department of Computer Science, University of Utah (1980). 3. Floyd R. W., Non-deterministic algorithms. J. ACM 14, 636-644 (1967). 4. Cohen J., Interpretation ofnon-deterministic algorithms in high level languages. Information Processing Lett. 3, 104-109 (1975). 5. Lafora Garcia F., Debuggers for high level languages. Ph.D. Dissertation, Computer Science Department, University of Pittsburgh (1982). About the Author--FERNANDOLAFORAreceived the Licenciado degree in mathematics from the Universidad de Madrid in 1975 and the M.Sc. and Ph.D. degrees in computer science from the University of Pittsburgh in 1980 and 1982, respectively. During his professional career, he has worked for the Consejo Superior de Investigaciones Cientificas (Superior Council of Scientific Research) and ITT Labs of Spain. He is currently working for NCR Spain. His research interests are software engineering and compiler construction. About the Author--MARY Lou SOVFAreceived the B.S. degree in mathematics from the University of Pittsburgh, a M.Sc. degree in mathematics from the Ohio State University and a Ph.D. in computer science from the University of Pittsburgh in 1977. Dr Sofia is currently an Associate Professor of computer science at the University of Pittsburgh. Her research interests are mainly in programming languages with a particular emphasis on the design, implementation and semantics of control structures. She is a member of ACM, SIGSOFT, SIGPLAN and Phi Beta Kappa.