home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Power-Programmierung
/
CD1.mdf
/
pascal
/
library
/
dos
/
bix
/
catch.doc
< prev
next >
Wrap
Text File
|
1986-08-04
|
19KB
|
503 lines
(*) 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