[comp.lang.eiffel] Exceptions

rick@tetrauk.UUCP (Rick Jones) (11/19/90)

(This is a re-post of an article I tried to send last week.  I don't think it
made it to anywhere on the net;  if anyone has already received it, I apologise
for the duplication.)
	------------------------------------------------------------

On one of my favourite subjects (as readers of this newsgroup may have noticed),
I have been trying to devise ways to extend the effectiveness of Eiffel's
exception handling.  As a result, I recently hit on a mechanism for passing
arbitrary objects from an exception occurrence to a rescue clause.  The idea
was remotely (only remotely :-) inspired by the proposed exception handling
system for C++.

After considering that this could be suitable as publicly contributed software
to Eiffel-shelf, etc. the implementation turned out to be so simple that I
thought I might as well just post it to the net.  What follows is largely my
notes on the system (written originally for internal consumption, excuse the
verbosity);  the source of the two key classes is appended.

I would of course like to hear any comments on this from interested parties
(before or after trying it).



	------------------------------------------------------------

	    Enhanced programmed exceptions for Eiffel
	    =========================================


Rick Jones						version: 1.1
Tetra Ltd.						date: 90/11/12


These classes provide an enhanced exception mechanism for programmed
exceptions.  It provides an extension to the "raise" facility, allowing
arbitrary objects to be passed between the raise and trap points of an
exception.  Exception propagation is also enhanced.  The standard "raise"
mechanism is used internally, the system is written entirely in conventional
Eiffel classes.


Classes

	Only two fundamental classes are involved, these are EXCEP and EXTRAP.

	EXCEP is a class which provides a basic exception object.  The creation
	of an EXCEP object implicitly raises an exception (i.e. calling such a
	Create will NOT return to the existing line of code).

	A class which wishes to trap exceptions resulting from creation of
	EXCEP objects must inherit from EXTRAP.  EXTRAP objects should NOT be
	instantiated.

	Note that a class which creates EXCEP objects does not need to inherit
	from any other class in order to use the facility.


Facilities

	EXTRAP provides a facility for descendants in the form of the feature
	"last_excep".  This should be used by code within a rescue clause, and
	returns an object reference of type EXCEP.  If the exception which
	causes the rescue clause to be invoked is the result of creating an
	exception of class EXCEP, "last_excep" will return the EXCEP object
	which was created at that time.  Otherwise it will return a Void.  It
	will also return a Void if called outside a rescue clause.


Examples

	The following code shows examples of using EXCEP and EXTRAP:

------------------------------------------------------------------------------

    class CATCH		-- test special exception catching

    inherit
	    STD_FILES ;
	    EXTRAP ;

    feature

	    Create is
	    local
		    excep: EXCEP ;
	    do
		    -- excep will only be non-void if it was assigned a value
		    -- in the rescue clause, indicating the "retry" condition
		    if excep.Void then
			    -- normal condition
			    try ;
		    else
			    -- we are being re-tried
			    putstring ("Trapped an exception\n") ;
			    -- will exit program
		    end ;

	    rescue
		    -- get the last exception - will only be non-void
		    -- if the exception was of class EXCEP
		    excep := last_excep ;

		    -- retry only if we found such an exception
		    if not excep.Void then retry end ;

		    putstring ("No retry - abort\n") ;
	    end ;

	    try is
		    -- a dummy routine to force an exception,
		    -- of course this need NOT be in the same class
		    -- as the routine which rescues the exception
	    local
		    excep: EXCEP ;
	    do
		    -- create an exception
		    excep.Create ;

		    -- there is no return from the Create above!
	    end ;
    end ;

------------------------------------------------------------------------------

	It should be noted that since creation of an EXCEP object always raises
	an exception, an EXCEP entity will in general only be non-void if
	assigned a value (directly or indirectly) via the use of "last_excep".
	Since this itself only yields a non-void result when used in a rescue
	clause, EXCEP objects are only ever accessible after they have been
	trapped following an exception.  If EXCEP references are only ever used
	as local entities, it is impossible to erroneously have an accessible
	EXCEP object when an exception has not occurred.


Inheriting from EXCEP

	Since this system passes back exception identifiers as objects, it is
	possible to define more complex exception objects by inheritance.  A
	programmer may define an exception class which inherits from EXCEP, and
	allow it to contain more complex information regarding the exception.

	As this is a general requirement, a class STD_EXCEP is provided which
	contains contain two simple attributes, "code" and "text", whose
	initial values are supplied in the call to Create.  These are arbitrary
	values to allow the programmer to identify different exceptions.

	The following is the code of the STD_EXCEP class:

------------------------------------------------------------------------------
    class STD_EXCEP		-- extended exceptions, standard class

	    -- this class provides an exception object with two attributes,
	    -- an integer and a string;  this should suffice for many cases
	    -- where some information needs to be attached to exceptions

    export
	    code, text, print_exception
		    -- standard features for coding exceptions

    inherit
	    EXCEP		-- general exception class

    feature

	    code: INTEGER ;		-- arbitrary code for exception
	    text: STRING ;		-- arbitrary text for exception

	    Create (c_code: INTEGER, c_text: STRING) is
		    -- general create, defining a code & text
	    do
		    code := c_code ; text := c_text ;
		    raisex (Current) ;
	    ensure
		    code = c_code ;
		    text = c_text ;
		    -- an exception is raised, there is no return from Create
	    end ;

	    print_exception( msg: STRING ) is
		    -- Print error code and text on standard error
		    -- with msg appended
	    external
		    fprintf: INTEGER language "C";
	    local
		    fmt: STRING;
		    res: INTEGER;
	    do
		    if not msg.Void then
			    fmt := "%d: %s: %s\n";
			    res := fprintf( io.error.file_pointer, fmt.to_c,
						    code, text.to_c, msg.to_c );
		    else
			    fmt := "%d: %s\n";
			    res := fprintf( io.error.file_pointer, fmt.to_c,
						    code, text.to_c );
		    end;
	    end;	-- print_exception

    end

------------------------------------------------------------------------------

	It is important that the Create routine of the new class either invokes
	the inherited (renamed) Create, or calls the inherited feature
	"raisex", passing "Current" as the parameter.  This will ensure that
	the exception is correctly raised.  It must also be the last thing
	which the Create routine does, as it will not return.

	When an exception is trapped, reverse assignment (the ?= operator) may
	be used to establish if the trapped exception is a specific descendant
	of EXCEP rather than treat it as the general case.  This may be done
	either in the rescue clause, or in the "retry" section of the routine
	body.

	A modification of the above example, where STD_EXCEP is raised, is
	shown below:

------------------------------------------------------------------------------

    class CATCH		-- test special exception catching

    inherit
	    STD_FILES ;
	    EXTRAP ;

    feature

	    Create is
	    local
		    excep: EXCEP ;
		    std_excep: STD_EXCEP ;
	    do
		    -- excep will only be non-void if it was assigned a value
		    -- in the rescue clause, indicating the "retry" condition
		    if excep.Void then
			    -- normal condition
			    try ;
		    else
			    -- we are being re-tried,
			    -- first treat as a general exception
			    putstring ("Trapped an exception\n") ;

			    -- is it a "standard" exception?
			    std_excep ?= excep ;
			    if not std_excep.Void then
				    std_excep.print_exception ;
			    end ;

			    -- will exit program
		    end ;

	    rescue
		    -- get the last exception - will only be non-void
		    -- if the exception was of class EXCEP
		    excep := last_excep ;

		    -- retry only if we found such an exception
		    if not excep.Void then retry end ;

		    putstring ("No retry - abort\n") ;
	    end ;

	    try is
		    -- a dummy routine to force an exception,
		    -- here the exception is a descendant of EXCEP
	    local
		    excep: STD_EXCEP ;
	    do
		    -- create an exception
		    excep.Create (1, "Exception 1") ;

		    -- there is no return from the Create above!
	    end ;
    end ;

------------------------------------------------------------------------------

	Application-specific exception classes may inherit either directly from
	EXCEP, or from STD_EXCEP, according to the requirements of the design.


Propagating an exception

	It is often a requirement to trap an exception, but rather than retry,
	propagate the exception back, maybe after doing some cleanup work in
	the rescue clause.  It may also be desirable to change the nature of
	the exception being propagated, but this should be done without
	actually raising a new exception within a rescue clause.  Exception
	substitution is required.

	The EXCEP class handles this completely transparently, allowing a new
	EXCEP object to be created while in a rescue clause.  If the exception
	being rescued is already of an EXCEP class, then the new exception
	object is substituted for the previous one and no new exception is
	raised.  This prevents the current rescue clause being re-entered, so
	avoiding indefinite loops.  It should be noted that in this situation
	the Create routine WILL return, allowing the exception to fall-back in
	the normal way.

	The following further adaptation of the previous examples shows this in
	cascading rescue clauses:

------------------------------------------------------------------------------

    class CATCH		-- test special exception catching

    inherit
	    STD_FILES ;
	    EXTRAP ;

    feature

	    Create is
	    local
		    excep: STD_EXCEP ;	-- only handles standard exceptions
	    do
		    -- excep will only be non-void if it was assigned a value
		    -- in the rescue clause -
		    -- this indicates the "retry" condition
		    if excep.Void then
			    -- normal condition
			    try ;
		    else
			    -- we are being re-tried,
			    putstring ("Trapped an exception\n") ;
			    putstring ("code = ") ;
			    putint (excep.code) ;
			    putstring (", text = ") ;
			    putstring (excep.text) ;
			    new_line ;
			    -- will exit program
		    end ;

	    rescue
		    -- get the last exception - will only be non-void
		    -- if the exception conforms to class STD_EXCEP
		    excep ?= last_excep ;

		    -- retry only if we found such an exception
		    if not excep.Void then retry end ;

		    putstring ("No retry - abort\n") ;
	    end ;

	    try is
		    -- a dummy routine to propagate an exception,
		    -- descendants of EXCEP which do not conform to
		    -- STD_EXCEP will be propagated back as the latter
	    local
		    other: SOME_CLASS ;
		    std_excep: STD_EXCEP ;
	    do
		    -- we presume that this call to other results in
		    -- an exception being raised
		    other.Create ;

	    rescue
		    -- see if the exception is of class STD_EXCEP, if not,
		    -- but of class EXCEP , propagate a standard exception
		    -- (an example of reverse assignment in the rescue clause)
		    std_excep ?= last_excep ;
		    if std_excep.Void and not last_excep.Void then
			    -- we propagate a general exception
			    std_excep.Create (0, "Undefined exception") ;
		    end ;

		    -- no retry, but the exception may have been changed
	    end ;
    end ;

------------------------------------------------------------------------------


Conclusions

	This mechanism provides a clean and simple way of passing exception
	objects to rescue clauses, using the standard "raise" mechanism of the
	language.

	It should be noted that as well as using "raise" as the exception
	mechanism, it depends on the current behaviour of the
	"is_programmer_exception" feature of class EXCEPTIONS.  This will never
	return True outside a rescue clause, since one of the effects of the
	"retry" statement is to clear the name of the raised exception (the
	string returned by the "programmer_exception_name" feature).  There is
	a case for not clearing this name on a retry when the simple "raise"
	exception mechanism is used, but for the purposes of this system the
	present method is preferable.

	There is also an interesting issue of the semantics of Create under
	exception conditions.  It is generally agreed that when an exception
	occurs during object creation the object in question should be
	inaccessible.  This is on the basis that the object can be presumed to
	be in an invalid state.  The current implementation of Eiffel does not
	in fact behave this way, but it is anticipated that it will in future.
	However, in this mechanism the exception object being created raises an
	exception but must continue to exist so that it can be accessed by the
	rescuing routine.

	There need not be a real conflict if the different conditions are
	examined.  In the normal case of Create, a reference to the new object
	is assigned to the entity associated with Create, e.g. the statements:

		a: A_CLASS
		a.Create

	can be considered equivalent to the notion of:
	
		a := new (A_CLASS)

	(a style of syntax used in some other OOPLs).

	If the creation of the object fails, the important point is that the
	entity "a" should remain void.  The fact that the object has been
	created and partially initialised will in the normal case result in it
	being collected by the garbage collector since it is unreachable.  If,
	however, the Create routine of A_CLASS deliberately chooses to store a
	reference to itself somewhere else then this need not be prevented,
	even under exception conditions.  This is the basis of the EXCEP class,
	since a reference to the new exception object is stored in a Once CELL,
	accessible to classes inheriting from EXTRAP, immediately before the
	exception is raised.

	In short, the fact that a Create routine never returns a reference for
	assignment to the invoking entity need not prevent the object existing
	and being referred to via other routes.


	------------------------------------------------------------

Source of EXTRAP:

indexing
	version: "1.2" ;
	date:	 "90/11/12" ;

class EXTRAP		-- an extended exception mechanism

	-- this class supports trapping of extended exceptions
	-- and must be inherited from by any class which contains a
	-- rescue clause to deal with these exceptions

inherit
	EXCEPTIONS	-- library exception class

feature

-- feature {NONE}

	stored_excep: CELL[EXCEP] is
		-- a common reference to the last exception
	once
		Result.Create ;
	end ;

-- feature {}		-- used only by EXCEP descendant classes

	magic_text: STRING is "EXCEPTIONAL" ;

	our_rescue: BOOLEAN is
		-- True if in a rescue clause and
		-- the trapped exception is one of ours
	do
		Result := is_programmer_exception (magic_text) ;
	end ;

-- feature {}		-- used only by application descendant classes

	last_excep: EXCEP is
	do
		if our_rescue then
			Result := stored_excep.item ;
		end ;
	end ;

end


	------------------------------------------------------------

Source of EXCEP:

indexing
	version: "1.2" ;
	date:	 "90/11/12" ;

class EXCEP		-- an extended exception mechanism

	-- this class provides objects which represent actual exceptions,
	-- the exception is raised when such an object is created;
	-- it may be inherited from to yield exception objects with
	-- special properties according to actual requirements

inherit
	EXTRAP		-- exception trap class

feature

	Create is
	do
		raisex (Current) ;
	ensure
		-- an exception is raised, there is no return from Create
	end ;

-- feature {}	-- may be called by descendants

	raisex (excep: like Current) is
		-- store and raise the given exception -
		-- if already rescuing, no raise is called
	do
		stored_excep.put (excep) ;
		if not our_rescue then
			raise (magic_text) ;
		end ;
	end ;

end

	------------------------------------------------------------
-- 
Rick Jones
Tetra Ltd.  Maidenhead, Berks, UK
rick@tetrauk.uucp	Absence of .signature is not .signature of absence