[Inform] Implementing exceptions

4 views
Skip to first unread message

Marnix Klooster

unread,
Oct 15, 1996, 3:00:00 AM10/15/96
to

Hello all,

Below I sketch a way to implement exceptions in Inform. Read my other
posts, and their replies, for the What and Why. It is essentially
simple, but I find it a little difficult to explain. I started to
write a description of the compilation process, but decided against
it. Learn by example, or fill my mailbox with complaints.

Terminology: the proposed try statement consists of two parts, the
try-clause and the exception handler; the latter has two parts, the
finally-clause and the except-clause, both of which are optional.

Three basic principles underlie my implementation:

(1) Exception names are encoded by numbers, so that a number uniquely
identifies an exception.

(2) There is a global variable (say ExcNum) which always contains the
(number of the) 'current exception'. This is the one that caused
control to be passed to the 'active handler'; the active handler is
the last entered handler that has not been left yet.

(3) When an exception is raised, this global variable is set to the
raised exception, and control is passed to the 'nearest' handler; this
handler should either handle it, or re-raise it. The nearest handler
is the handler of the last entered try-clause that has not been left
yet.

There are two problems to be solved: how to pass control to the right
handler on raising an exception, and how to maintain the current
exception correctly.

The rest of this mail consists of two parts: a program in Inform-with-
exceptions, and a translation into regular Inform. The translation is
heavily commented, and should give you an idea how exceptions can be
implemented.

First, here is the program as I would like to use it in Inform. I'll
use the syntax I first proposed here, for clarity. See my reply to
Andrew's comments for a discussion of other syntaxes.

!-------------------------------------------------------------------

Exception DivisionByZero;
Exception Test;

[ Main;
print "Division^";
print "An Exceptional Demonstration Of Mathematical Truths^^";

PrintDiv(32767,217);
PrintDiv(1,0);
];

! Print the result of the division a/b.

[ PrintDiv a b
c;
print "Dividing ", a, " by ", b, " results in ";
try {
try
c = DoDiv(a,b);
except {
None: print c;
DivisionByZero:
print "fail";
raise Test
}
} except {
Test: print "ure";
}
print ".^";
];

! Return a/b, or raise DivisionByZero if b==0.

[ DoDiv a b;
if (b==0)
raise DivisionByZero;
return a/b;
];

!-------------------------------------------------------------------

The above program should produce the following output:

Division
An Exceptional Demonstration Of Mathematical Truths

Dividing 32767 by 217 results in 151.
Dividing 1 by 0 results in failure.

The rest of this file is a translation of the above imaginary Inform
program into regular Inform.

!-------------------------------------------------------------------

! The following global variables are used by the implementation.

Global ExcNum; ! The 'current exception'
Global LastFP; ! The last caught frame pointer
Global NumFP = 0; ! The number of frame pointers caught
Global CallResult; ! Temporary location for call result


! Now we declare the built-in and user-declared exceptions as
! constants. We use numbers from 0 upwards, but the only requirement
! is that they are unique.

Constant None = 0; ! built-in
Constant DivisionByZero = 1;
Constant Test = 2;


! The Main routine is unchanged.

[ Main;
print "Division^";
print "An Exceptional Demonstration Of Mathematical Truths^^";

PrintDiv(32767,217);
PrintDiv(1,0);
];


! The original PrintDiv routine contains two try statements, which we
! have to emulate here.

[ PrintDiv a b
c;
print "Dividing ", a, " by ", b, " results in ";

! Here begins a try statement. During this statement we save the
! current exception on the program stack. If the try statement
! terminates normally, we must restore it. (If it raises an
! exception, there is no need to.) We do this on leaving the
! handler, either at the end, or when an exception is raised from
! the handler.

@push ExcNum;

! During the try-clause we also remember the current frame pointer,
! since this may be overwritten during calls from within this
! clause.

@push LastFP;

! Again, here begins a try statement, so we save ExcNum.

@push ExcNum;

! And again we enter a try-clause, so we save the frame pointer.

@push LastFP;

! Now we must perform the call DoDiv(a,b) within a try, and
! store the result in c. This is compiled by a call to a
! CatchCall routine, which in effect calls the routine, and
! returns true if an exception was raised, and false
! otherwise. The result of the original call is stored in
! CallResult.

! A side effect of this call is that LastFP is changed. NumFP
! is not changed, however. ExcNum is only changed if an
! exception is raised within the called routine (here DoDiv).

if (CatchCall2 (DoDiv, a, b))

! If the call raised an exception it has been stored in
! ExcNum, and we go straight to the handler.

jump handler;

! Store the result of the call.

c = CallResult;

! Now we have reached the end of the try clause without
! raising an exception. Therefore we raise the built-in
! exception None, and go to the handler.

ExcNum = None;

.handler;

! Here begins the inner handler. First thing to do is to
! restore LastFP, which may have been changed by CALLs in the
! try clause.

@pull LastFP;

! (If there had been a finally-clause, its code would have been
! inserted here.)

! Now begins the inner except clause. It is implemented
! simply as a switch on the current exception. The actual code
! is unchanged.

switch (ExcNum) {
None: print c;
DivisionByZero:
print "fail";

! Our first raise statement. To raise Test, we have to set
! ExcNum to it, and jump to the currently active handler.
! In this case, this is the handler of the outer try
! statement, which we can jump to.

! But as noted above we have to be careful: we leave this
! handler, so we must make sure to throw away the exception
! number which we saved on the stack before entering this
! try statement.

ExcNum = Test;
@pull temp_global; ! the Inform built-in scratch variable
jump handler2;

default:

! There was no default case, so we substitute the implicit
! one: pass the exception on. In other words, we re-raise
! the current exception. Thus we do the same as with the
! previous raise statement, except that we do not change
! ExcNum.

@pull temp_global;
jump handler2;
}

! We leave the inner try statement here, so we must restore
! ExcNum to its previous value.

@pull ExcNum;

! We leave the outer try clause, so we "raise None".

ExcNum = None;

.handler2;

! Entering the outer handler, which means restoring LastFP and
! switching on the exception number.

@pull LastFP;

! (If there had been a finally-clause, its code would have been
! inserted here.)

switch (ExcNum) {
Test: print "ure";

None:

! Now, the programmer specified no case for None, so we
! provide the implicit one: do nothing.

;

default:

! There is no default case either, so we provide the implicit
! one: re-raise the current exception.

! While above we could use a jump to the outer handler, this
! is not possible here: this raise does not occur textually
! within a try clause. Therefore we call Raise(), which uses
! a THROW with LastFP to return true from the latest call to
! CatchCall.

! Note that while we leave the try statement here, there is no
! need to pop the saved exception number from the stack,
! because this routine's program stack is THROWn away anyway.

Raise();
}

! We exit this try statement, so we restore the exception number...

@pull ExcNum;

! ...and go on with our regular schedule.

print ".^";
];


! The original DoDiv contains an explicit raise statement.

[ DoDiv a b;
if (b==0) {

! As above in the implicit default case of the outer handler,
! raising an exception not within a try clause simply means
! setting ExcNum and calling Raise().

ExcNum = DivisionByZero;
Raise();
}
return a/b;
];


! Auxiliary functions
! -------------------

! A CatchCall routine takes a routine address and a number of
! arguments. It either stores the result of the call in global
! variable CallResult and returns false; or it stores the raised
! exception in ExcNum and returns true. In both cases LastFP is
! overwritten, but NumFP is unchanged.

! There must be a version of CatchCall for every number of arguments
! from 0 to 7, or one could use CHECK_ARG_COUNT to find out the number
! of arguments passed. The latter method has more runtime overhead,
! and uses about the same amount of code. (Note that to have a
! CatchCall7 we need to store the 7th argument in some temporary
! global variable before the call, and read it in CatchCall7.)

! Note that NumFP is necessary to know in Raise() whether LastFP
! really contains a frame pointer. It is non-zero in that case, and
! zero otherwise. It is incremented on doing a call in CatchCall, and
! decremented either in CatchCall (on normal return) or in Raise (on
! raising an exception).

[ CatchCall2 r a b;

! Catch the current frame pointer, store it in LastFP, and increment
! NumFP.

@catch LastFP;
NumFP++;

! Perform the actual call.

@call_vs r a b CallResult;

! Decrement NumFP, and return false to indicate that all went well.

NumFP--;
rfalse;
];


! Raise() return true from the latest CatchCall, or prints an
! "uncaught exception" message if there was no previous CatchCall.
! Note that a call to Raise() never returns in the routine where it
! occurs.

[ Raise;

! If NumFP is zero, a Raise is performed from a routine that wasn't
! called from CatchCall (either directly or indirectly). Thus we
! have an uncaught exception.

if (~~NumFP) {
print "Uncaught exception: ", (exc) ExcNum, "^";
quit;
}

! The called routine will be terminated abnormally, so update NumFP
! first, and then return true from the latest CatchCall.

NumFP--;
@throw true LastFP;
];


! We provide a routine to print the name of an exception; this is used
! in the event of an uncaught exception, in Raise above. This could
! very well be exposed to the programmer, to be used as a debugging
! aid.

[ exc x;
switch (x) {
None: print "None";
DivisionByZero: print "DivisionByZero";
Test: print "Test";
default: print "illegal #", ExcNum;
}
];

!-------------------------------------------------------------------

That was the example. Hope this clarifies things a bit.

Groetjes,

<><

Marnix
--
Marnix Klooster
kloo...@dutiba.twi.tudelft.nl // mar...@worldonline.nl


Marnix Klooster

unread,
Oct 15, 1996, 3:00:00 AM10/15/96
to

[Repost: previous post was incomplete and has been cancelled.]

Less than 24 hours ago I posted the following:

> Below I sketch a way to implement exceptions in Inform. Read my other
> posts, and their replies, for the What and Why. It is essentially
> simple, but I find it a little difficult to explain. I started to
> write a description of the compilation process, but decided against
> it. Learn by example, or fill my mailbox with complaints.

and followed this by a short description and an example. What I
forgot to say is what the characteristics of my implementation idea
are:

* No modification to the Z-machine is necessary; the only condition
is that the @catch and @throw instructions can be used. This implies
that this scheme works only for versions 5 and higher, and that one
needs an interpreter that implements these instructions correctly.
(On the latter point, see my post "[Z-machine] Catch & throw".)

* If exceptions are used the resulting game file will be slightly
larger, because of the inclusion of some auxiliary routines. But on
the scale of games like Jigsaw and So Far this can safely be ignored.

* As long as no exceptions are actually raised, overhead is incurred
only on CALLs that are textually within a try-clause (i.e., within the
"try {...}" part of a try statement).

* When an exception is raised, control is passed from handler to
handler, 'inside-out,' until one is found that handles the situation.
(If none is found, an "uncaught exception" message is printed.) The
delay may in some cases be noticable, especially if many try
statements are nested. Thus over-use of exceptions must be avoided,
if one is interested in efficient execution.

* It should be not too difficult to extend the Inform compiler to
support exceptions in this way, since the compilation process for most
constructs remains the same. (This can be seen by comparing the
original example and its translation.)

A final note: my example translation uses global variables to store
data used by the exception mechanism. But other memory locations can
be used for this instead, if global variables are in short supply.
(Are they? Does the Inform compiler use memory locations instead of
Z-machine global variables if the programmer specifies more than 240
variables? Should it?)

Summarizing, I think it is a simple and efficient implementation of
Inform exceptions. But if you have better ideas, out with them!

Groetjes,

<><

Marnix
--
Marnix Klooster
mar...@worldonline.nl


Reply all
Reply to author
Forward
0 new messages