home *** CD-ROM | disk | FTP | other *** search
-
-
-
-
- (*) Turbo Catch and Throw
- (*)
- (*) by Andrew Feldstein
- (*)
- (*) Version 1.0
- (*)
- (*) December 18, 1985
- (*)
-
-
- I. DESCRIPTION
-
- A. Usage
-
- This section defines the basic use of the procedures CATCH,
- THROW, and UNCATCH and the function CAUGHT as supplied in the
- module catch.pas. Succeeding sections contain a more detailed
- description.
-
- CATCH is called with a parameter of type CATCHDATA. Often,
- this will be global to avoid the inconvenience of passing it as a
- parameter to every procedure in the program.
-
- A THROW later called on CATCHDATA previously initialized by CATCH
- will unwind the stack until the procedure in which the data was
- originally initialized is on the top of the stack; control will
- be transferred to the statement directly succeeding the call to
- CATCH. THROW can be called on a CATCHDATA any number of times for
- any one call to CATCH.
-
- CAUGHT is a boolean function which takes a CATCHDATA as a
- parameter. The function returns false if the statement
- succeeding the CATCH call was reached through CATCH itself; the
- function returns true if the statement was reached via a call to
- THROW.
-
- UNCATCH should be called by any procedure which calls CATCH
- before that procedure exits. However, this is not strictly
- necessary--it simply ensures that it is ERROR for any later THROW
- on that particular incarnation of CATCHDATA.
-
-
- B. Overview
-
- When CATCH is called the status of the stack and control
- variables is remembered. When THROW is called, the stack is
- unwound until the destination procedure's frame is the current
- frame and control is transferred to the statement directly
- succeeding the prior call to CATCH.
-
- Thus, the THROW procedure performs a goto in the sense that any
- call to the procedure CATCH can be said to define a label
- pointing to the statement succeeding it. THROW functionally
- performs a goto to that label. However, this goto is not without
- restriction as the goto statement proper is. An unrestricted
- goto allows a jump to any location in the program, whether or not
- that location lies within the lexical or dynamic scope of the
- block in which the goto statement is executed.
-
- Lexical scope in Pascal is defined as the currently defined block
- and all blocks global to it. For example, if procedure C is a
- sub-procedure of procedure B, procedure B is a sub-procedure of
- procedure A, and procedure A is defined in the global program
- block, then the lexical scope of procedure B extends to the
- declarations (and, for the purposes of goto, the code) of the
- global program block, of procedure A, and of procedure B itself.
- It does not extend to the declarations within procedure C. Turbo
- Pascal allows goto statements whose target location is within the
- current block, but not to any other block.
-
- Dynamic scope is defined as the currently executing block, and
- all those other blocks that are also currently active--i.e. all
- those blocks which through the chain of calling have led to the
- current procedure being executed. For example, given the
- definitions of A, B, and C in the previous paragraph, if the
- code defined in procedure B is executing, then the dynamic scope
- of procedure B AT THAT TIME is B, A, and the global block.
-
- The goto performed by THROW is restricted to locations within the
- dynamic scope of the currently executing procedure--the procedure
- which called CATCH must not have exited when THROW is
- called! Even if the procedure which once called CATCH has exited
- and been entered again, it would be error for a THROW on any
- CATCHDATA not initialized by the current incarnation of the
- procedure calling CATCH.
-
- NOTE: This means, among other things, that CATCH cannot be
- conveniently called from within error procedures (for example
- those in example.pas). CATCH ought to be treated as a label.
-
- While it is an error for a THROW to be made out of dynamic scope,
- there is no convenient way in the Turbo runtime system to check
- for this. However, with judicious use of the UNCATCH procedure,
- the user can effect this type of error checking: if the UNCATCH
- procedure is called on any CATCHDATA previously initialized by
- CATCH when that CATCHDATA is no longer relevant, subsequent throws
- on that CATCHDATA will cause an error.
-
- It is often said, "Catch and Throw is a structured form of
- goto." As shown above, it is at least a form of GOTO. However,
- that it is "structured" is true only in the sense that it is
- implemented with subroutines and not mere jumps. Consider: With
- goto proper, one can at least determine by looking at the goto
- statement where control will end up. With a THROW, on the other
- hand, because the transfer of control is functionally an indirect
- jump one cannot tell. If there are several places where CATCH
- may have been called, then there are several places to which
- control may be transferred. Further, with goto many statements
- may jump to a single location, but each jump is to only one
- location. With THROW, not only may many statements jump to a
- single location but each jump may be to many locations. Thus,
- the possibility of "spaghetti" code with Catch and Trhow is
- ultimately even greater than with goto!
-
- Is this then "structured?" Yes and no. It is structured if the
- possible horribles suggested above are minimized and the power of
- the mechanism is reigned in by a few well-defined entry and exit
- mechanisms (such as are found in the error procedures of
- example.pas). The following example, however, although illustrative
- of how THROW transfers control, is otherwise a good example of bad
- use.
-
-
- C. A Minimal Example
-
- { The following little program prints out "Strike any key to
- exit." until a key is pressed. It then prints out "Goodbye." and
- exits. A more sophisticated example of error processing with
- Catch and Throw (and a better example of its use) is supplied in
- the file "example.pas." The example here is meant merely to
- demonstrate how Catch and Throw are used. }
-
- {$C-}
- {$I CATCH.PAS}
-
- var cd : catchdata;
-
- procedure messagecheck;
- var eatchar : char;
- begin
- writeln('Strike any key to exit.');
- if keypressed then
- begin
- read(kbd,eatchar); { Gobble the character pressed. }
- throw(cd)
- end
- end;
-
- procedure main;
- var c : char;
- begin
- catch(cd);
- if caught(cd) then
- begin
- writeln('Goodbye.');
- exit
- end;
- repeat
- messagecheck
- until false
- end;
-
- begin main end.
-
-
-
- II. IMPLEMENTATION
-
- A. Pascal Considerations
-
- -- Save data globally or locally:
-
- Should CATCH save the necessary data in a single, private
- location, or should it save it in a user defined data structure?
- As useful as being able to simultaneous throw to only one
- location is, it is infinitely more useful to be able to throw to
- more than one location at any one time. (For example an error
- procedure can maintain a stack of locations). Thus, the catch
- and throw procedures are implemented with a single parameter
- containing the state information necessary to do the throw.
-
- -- Implementation of CATCH:
-
- Should CATCH be declared as a procedure or function?
-
- It is generally useful for the program after a return from the
- CATCH procedure to distinguish whether the return was from the
- original call to CATCH or because of some THROW. One way to do
- this is for a procedure calling CATCH to maintain some sort of
- state variable, but this is messy as well as unintuitive and hard
- to implement. Some implementations make CATCH a boolean
- function. The calling procedure invokes it thus: "if CATCH
- then [original call] else [call via THROW]." This has a language
- problem and an implementation problem.
-
- The language problem is that very often it is unnecessary to to
- differentiate among returns from an original call to CATCH and
- returns from CATCH via a call to THROW. It is messy to force a
- procedure to declare a boolean variable just to call CATCH:
- "dummy_boolean := CATCH."
-
- The implementation problem is that if CATCH is a function then
- the THROW will necessarily be into a boolean expression while if
- CATCH is a procedure then the location it returns to is the
- beginning of a statement--a much cleaner concept. The problem
- with jumping to the middle of an expression should be manifest:
- If the boolean expression is complex, then the other elements of
- the expression will not have been evaluated even though they
- ought to have (e.g. ((A=B) AND CATCH)--(A=B) will not have been
- evaluated). Even if the boolean expression is simply a call to
- CATCH there is the problem of how a particular implementation
- handles function results--it is much easier for the THROW
- procedure to simulate return from a procedure than from a
- function.
-
- The simple solution I have chosen for this problem is to provide
- the function CAUGHT (which, incidentally, takes the same type of
- parameter as CATCH and THROW) and returns true if the last
- procedure called on the particular CATCHDATA was a THROW and false
- if it was CATCH.
-
- --Checking that CATCH previously called on data passed to THROW:
-
- Finally I have provided some error protection for those (
- grantedly improper) cases where THROW might be called without a
- prior call to CATCH, or where the procedure calling CATCH has
- exited. Both of these ought semantically never to happen, yet
- through inadvertence they might. Therefore, the CATCH procedure
- sets special signature bytes within the data saved so that THROW
- may recognize that it has valid data. This provides a minimal
- level of detection of data being inadvertently written over.
-
- -- Checking that THROW is within dynamic scope:
-
- As a last protection, I have provided the procedure UNCATCH which
- operates to invalidate a previous CATCH. This protects the
- system in the case that the data in which the state is saved is
- global (very common), the procecdure which called CATCH might
- exit removing its frame from the stack, and THROW is errantly
- called. A procedure which calls CATCH should call UNCATCH before
- it exits to invoke this protection. Use of UNCATCH is especially
- recommended when used in Turbo's memory compile mode as global
- data will remain initialized from a previous running.
-
- When THROW is called on data on which either CATCH has never been
- called or on which UNCATCH has been called, an error message is
- output and the program is halted. While the current (simple)
- errorchecking is sufficient to detect the most blatant errors,
- possible further errorchecking should be done to determine the
- subtler errors (perhaps a checksum).
-
- WARNING: CATCH and THROW as currently implemented are not
- designed to be called from within interrupts even though they
- would be useful there (imagine hooking up the ^BREAK interrupt to
- THROW, for example).
-
- B. Representation of Environments
-
- A Turbo program may loosely be seen to execute within several
- environments:
-
- - A control environment
- - A frame environment
- - A global data environment
- - A local data environment
- - A dynamic data environment
-
- CATCH remembers a particular state of the control and frame
- environments which may then subsequently be restored by THROW.
- Thus, to implement CATCH it is sufficient to store the states of
- the control and frame environments--and to implement THROW it is
- sufficient to restore them. The problem now arises of how Turbo
- defines each of these environments.
-
- The Turbo manual in discussing external procedures and inline
- statements describes the SP, BP, DS, and SS registers as
- "critical" (i.e. should be left unchanged). Furthermore, it is
- obvious that the CS and IP (instruction pointer) registers are
- also critical (but their values are always implied in the context
- of inline statements and external functions so the manual does
- not need to refer to them). The architecture of the 8088 and the
- discussions and examples in the Turbo manual suggest that the
- control environment is defined by the CS and IP registers and
- that the frame environment is defined by the SS, SP, and BP
- registers (with the current frame defined by the SS and BP
- registers).
-
- Because the DS register does not define either the control or
- frame environments it may be safely ignored. But it may be
- ignored for yet another reason: it never changes. Neither do
- the SS and CS registers which do help define the control and
- frame environments. Thus, SS and CS may be ignored since CATCH
- knows that the values will be the same when and if a call to
- THROW ever comes about. It is only necessary to save and restore
- the SP, BP, and IP registers to implement Catch and Throw. (Note
- that part of the problem of using CATCH or THROW from within an
- interrupt routine is that these assumptions break down.)
-
- C. Implementation of CATCH and THROW
-
- The values stored by CATCH are contained with in the appropriate
- fields of variables of type CATCHDATA. Transfers to and from the
- registers are done via typed constants (which reside in the code
- segment). The indirection is necessary since I did not want to
- "hardwire" the structure of the CATCHDATA into the inline
- statements. By doing it this way, everything can be done by name
- and not by numerical offset.
-
- Furthermore, I find that the machine language in the inline
- statements is simpler if the code segment is addressed with a
- simple offset than if the stack segmente is addressed with an
- offset off the base pointer register.
-
- Turbo assembles the CATCH procedure which has three typed,
- two-byte constants and one VAR parameter as follows:
-
- PUSH BP
- MOV BP,SP
- PUSH BP
- JMP BEGIN
- DW 0
- DW 0
- DW 0
- BEGIN: [Compiled code]
- JMP END
- END: MOV SP,BP
- POP BP
- RET 0004
-
- Before the first statement of compiled code is executed, the stack is
- set up as follows (offsets are relative to current BP):
-
- (+04) parameters
- (+02) return address
- (+00) Original BP
- (-02) Original SP (less 2 because of push of original BP)
- (-04) top of workarea
-
- For procedure CATCH, the return address of the calling procedure is at
- BP+02; the BP of the calling procedure is at BP+00; the SP of the calling
- procedure is the current value of BP plus 8 bytes (for saved BP return
- address, and parameter. These values are stored in the
- corresponding fields of the CATCHDATA.
-
- The CATCH procedure is implemented so as to store the actual
- values that THROW should restore. All THROW then has to do is
- determine that it has valid data and store the values in the
- appropriate registers. The IP register itself is loaded with a
- jump instruction.
-
-
-
- $$$$$$$$$$$$$$$$ File: EXAMPLE.PAS
-
-
- {$Icatch.pas}
-
- { Maximum number of catchdata in stack }
- const errorstackmax = 50;
- { Turbo string definition for error messages }
- type lstring = string[255];
- { Error stack of catch data }
- var errorstack : array[1..errorstackmax] of ^ catchdata;
- { Initialize stack to empty }
- const errorstacksize : integer = 0;
-
- {----------------------------------------------------------------}
-
- { Push the catchdata on the stack. Take the address of the
- variable parameter with the built in "ptr" function. The stack
- contains only pointers to the catchdata for two reasons--to
- minimize space and to keep only one copy of any one catchdata
- around. }
-
- procedure errorpush({IN}var cd : catchdata);
- begin
- if errorstacksize = errorstackmax then begin {Error Stack Overflow} end;
- errorstacksize := succ(errorstacksize);
- errorstack[errorstacksize] := ptr(seg(cd),ofs(cd))
- end;
-
- {----------------------------------------------------------------}
-
- { Pop the topmost catchdata on the stack. }
-
- procedure errorpop;
- begin
- if errorstacksize = 0 then
- begin {Error Stack Underflow} end
- else
- errorstacksize := pred(errorstacksize)
- end;
-
- {----------------------------------------------------------------}
-
- { Does a throw to the catchdata on the top of the stack while
- popping it off. }
-
- procedure errorcontinue;
- begin
- errorpop;
- throw(errorstack[succ(errorstacksize)]^)
- end;
-
- {----------------------------------------------------------------}
-
- { Writes out error message and does a throw to the catchdata on
- the top of the stack while popping it off. }
-
- procedure error(errormessage : lstring);
- begin
- writeln(con,errormessage);
- errorcontinue
- end;
-
- {----------------------------------------------------------------}
-
- { If the code in this procedure, or any procedure called by this
- procedure raises an error, then when THROW is executed, control
- will be passed to some ultimate caller of this procedure and the
- stack frame for this procedure will have been removed from the
- stack. }
-
- procedure example1;
- begin
- { . . . }
- end;
-
- {----------------------------------------------------------------}
-
- { This procedure represents an example of a situation where it
- would be intolerable for any error raised within it to THROW
- execution to some ultimate caller of this procedure because a
- (possibly) open file would then never be closed ultimately
- exhausting the very limited resource of open files. Another
- example would be a situation in which memory was allocated but
- not yet used (a throw around that procedure would result in a
- dangling pointer).
-
- If code represented by the ellipsis raises an error, it will be
- caught, the file will be closed and error execution will continue
- with the next previous procedure to have called CATCH and
- ERRORPUSH. Note that the CATCH and the ERRORPUSH are set up only
- when it is critical that there is no throw around this
- procedure--i.e. only when the REWRITE of OUTFIL is successful. }
-
- procedure example2;
- var outfil : text;
- cd : catchdata;
- begin
- assign(outfil,'filename.ext');
- {$I-} rewrite(outfil); {$I+}
- if IOresult <> 0 then
- error('IO error: cannot write file.');
- catch(cd);
- if not caught(cd) then
- errorpush(cd) { Return is from catch so push errordata }
- else
- begin { Return is from throw so close file and continue }
- close(outfil);
- errorcontinue
- end;
-
- { . . . }
-
- errorpop;
- uncatch(cd)
- end;
-
- {----------------------------------------------------------------}
-
- { This procedure unconditionally traps all errors and continues
- execution with the statement following ERRORPUSH (ignoring
- whether that point was reached via CATCH or THROW). Thus any
- error raised by the code represented by the ellipsis would result
- in that code being reexecuted. A common example of this situation
- would be a main command loop.
-
- Note that if, as is often the case, throw is only ever to one place,
- for example to the top of a main command loop, then stacking the
- catchdata is unnecessary and catch and throw should be used in their
- raw form. }
-
- procedure example3;
- var cd : catchdata;
- begin
- catch(cd);
- errorpush(cd);
-
- { . . . }
-
- errorpop;
- uncatch(cd)
- end;
-
-
-
- $$$$$$$$$$$$$$$$ End
-