Exceptions as Objects

Revision as of 03:17, 21 January 2016 by Manus (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Overview

This article describes many aspects of Exceptions as Objects (ECMA-367 8.26) which will be implemented in ISE Eiffel. All exceptions will be raised as objects which can be accessed by `last_exception'. Exceptions as objects are known as type EXCEPTION and its descendants. EXCEPTION_MANAGER is introduced to handle common exception class operations, i.e. catch, and ignore.

This doc is from the original working version: http://docs.google.com/Doc?docid=dckspcv8_9htpfm4&hl=en

Exception class hierarchy

 EXCEPTION
    SYSTEM_EXCEPTION
       MACHINE_EXCEPTION
          OPERATING_SYSTEM_EXCEPTION
             COM_FAILURE
             OPERATING_SYSTEM_FAILURE
             OPERATING_SYSTEM_SIGNAL_FAILURE
          HARDWARE_EXCEPTION
             FLOATING_POINT_FAILURE
       EIFFEL_EXCEPTION
          LANGUAGE_EXCEPTION
             BAD_INSPECT_VALUE
             VOID_TARGET
             VOID_ASSIGNED_TO_EXPANDED
             ROUTINE_FAILURE
             EIFFELSTUDIO_SPECIFIC_LANGUAGE_EXCEPTION
                CREATE_ON_DEFERRED
                ADDRESS_APPLIED_TO_MELTED_FEATURE
          EIFFEL_RUNTIME_EXCEPTION
             NO_MORE_MEMORY
             DATA_EXCEPTION
                IO_FAILURE
                SERIALIZATION_FAILURE
                MISMATCH_FAILURE
             EXTERNAL_FAILURE-- Only used for threads at the moment
             EIFFEL_RUNTIME_PANIC               
   ASSERTION_VIOLATION
      INVARIANT_VIOLATION
      PRECONDITION_VIOLATION
      POSTCONDITION_VIOLATION
      CHECK_VIOLATION
      VARIANT_VIOLATION
      OLD_VIOLATION
      LOOP_INVARIANT_VIOLATION
   DEVELOPER_EXCEPTION
   OBSOLETE_EXCEPTION     
      RESUMPTION_FAILURE Was RESUMPTION_FAILED.
      RESCUE_FAILURE --------- SHOULD NOT APPLY ANY MORE WITH ISO/ECMA, CHECK!!!
      EXCEPTION_IN_SIGNAL_HANDLER_FAILURE

Classes in red plus EXCEPTION_MANAGER will be placed at base\elks\kernel\exceptions\. The rests are ise specific.
Terminology We use _EXCEPTION for intermediate nodes in the hierarchy; when we need a term in the leaves of the hierarchy we don't use EXCEPTION but

   * _VIOLATION for assertions (also used for ASSERTION_VIOLATION, an intermediate node)
   * _FAILURE otherwise
 
 This is consistent with Eiffel exception terminology since from the viewpoint of Eiffel these things have failed, for example a signal was not handled properly. The Eiffel runtime is causing an exception in the Eiffel code as a result, but the original event was a failure.  Hence SERIALIZATION_FAILURE etc.

Raise and catch an exception

Catching an exception

All exceptions can possibly be caught. `is_caught' is set by default. Only the given type of exception is `ignore'd in EXCEPTION_MANAGER if possible, it hence is not caught. The caught exception can be accessed by `last_exception'.

Raising an exception

There are two types of exceptions concerning how an exception is raised.

  • System raised

Exception raised through the runtime. Most exceptions of this type are predefine as classes in base library. i.e. assertion violations, no memory exceptions and routine failures.

  • User raised

User raised exceptions are initialized by users and raised by explicit call of {EXCEPTION}.raise.

Theoretically the runtime can raise any exceptions known. A user can only raise an EXCEPTION that is `is_raisable'. `is_raisable' is possible true only when the exception is of DEVELOPER_EXCEPTION. This means only raising a DEVELOPER_EXCEPTION or its descendant is guaranteed correct.

Ignoring, continuing an exception

Quote from ECMA 8.26.12:

 It is possible, through routines of the Kernel Library class EXCEPTION, to ensure that exceptions of certain types be:
 
 * Ignored: lead to no change of non-exception semantics.
 * Continued: lead to execution of a programmer-specified routine, then to continuation of the execution according to non-exception semantics.

An ignorable exception can be ignored. When an exception is ignored, the raising does nothing as if non-exception semantics. What are the exceptions not ignorable?

Related queries in EXCEPTION:

is_ignored: BOOLEAN
is_caught: BOOLEAN
is_ignorable: BOOLEAN

Related routines in EXCEPTION_MANAGER:

is_caught (a_exception: TYPE [EXCEPTION]): BOOLEAN
is_ignored (a_exception: TYPE [EXCEPTION]): BOOLEAN
is_ignorable (a_exception: TYPE [EXCEPTION]): BOOLEAN
catch (a_exception: TYPE [EXCEPTION]) 
ignore (a_exception: TYPE [EXCEPTION]) 
set_is_ignored (a_exception: TYPE [EXCEPTION]; a_ignored: BOOLEAN)

`origin' VS `cause'

  • The origin of an exception is, among all exceptions that have so far been triggered but not handled, the most recent one that is not a ROUTINE_FAILURE. Because a ROUTINE_FAILURE is always the result of an exception of another kind, the `origin' is always defined.
  • The `cause' of an exception is only interesting in the case of an exception triggered in a rescue clause. In that case the exception is the result of incomplete handling of another exception (which might be a ROUTINE_FAILURE or any other kind). Then the `cause' is that other exception.
  • If an exception was not triggered in a rescue clause, its `cause' is the exception itself.

Exception at rescue clause

See the following diagram:

Exception in rescue.png

Exception e2 is kept as `cause' at d.f4 in which e3 is raised, and being called in rescue clause r2. In this case e3 is accessible by `last_exception' and e2 is accessible by `cause' of e3.

A behavior should be clarified, that when an exception occurs deep in execution level of a rescue clause, `last_exception' is understood as "last unhandled exception".
1. A new exception causes "last_exception" to be saved somewhere before entering "rescue" clause. After that the new exception object is attached to "last_exception".

2. If an exception is not handled (there is no rescue clause or "retry" is not performed), a new exception object "ROUTINE_FAILURE" is created, its attribute "original_exception" is set to "last_exception", and "last_exception" is set to this new "ROUTINE_FAILURE" object.

3. If an exception is handled ("retry" is executed), "last_exception" is attached a value saved in step 1.

Example: If "last_exception" in the following code snippet is modified by the call to feature "h" that raises an exception internally and handles it?

rescue
                -- "last_exception" is set to "X".
                -- Call feature that raises exception "Y", but handles it.
            h
 
                -- Is "last_exception" set to "X" or to "Y"?
                -- The answer is X.

OLD_VIOLATION

OLD_VIOLATION behaves similarly with ROUTINE_FAILURE. In other words, OLD_VIOLATION is never a "root" exception of an exception chain. According to ECMA 8.26.11, OLD_VIOLATION should be treated as a "root" exception. But I believe it is more interesting to know the cause (original exception) of the old voilation. This will be clarified soon.

Class ANY

This is now done in EXCEPTION_MANAGER.

class ANY
...
feature -- Exception
 
    frozen last_exception: EXCEPTION
            -- Last exception
        external
            "built_in"
        end
...

Class EXCEPTION

deferred class interface
	EXCEPTION
 
feature -- Access
 
	code: INTEGER_32
			-- Code of the exception.
			-- |Maybe we don't need this anymore
 
	exception_trace: STRING_8
			-- String representation of current exception trace
 
	meaning: STRING_8
			-- A message in English describing what `except' is
 
	message: STRING_8
			-- Tag of current exception
 
	original: EXCEPTION
			-- Original exception that triggered current exception
 
	cause: EXCEPTION
			-- Unhandled exception before current exception was triggered in a rescue clause
 
	recipient_name: STRING_8
			-- Name of the routine whose execution was
			-- interrupted by current exception
 
	type_name: STRING_8
			-- Name of the class that includes the recipient
			-- of original form of current exception
 
feature -- Status report
 
	is_caught: BOOLEAN
			-- If set, exception is raised.
		ensure
			not_is_caught_implies_is_ignorable: not Result implies is_ignorable
			not_is_ignored: not is_ignored
 
	is_ignorable: BOOLEAN
			-- Is current exception ignorable?
 
	is_ignored: BOOLEAN
			-- If set, no exception is raised.
		ensure
			is_ignored_implies_is_ignorable: Result implies is_ignorable
			not_is_caught: not is_caught
 
	is_raisable: BOOLEAN
			-- Is current exception raisable by raise?
 
feature -- Raise
 
	raise
			-- Raise current exception
		require
			is_raisable: is_raisable
 
end -- class EXCEPTION

Class EXCEPTION_MANAGER

class interface
	EXCEPTION_MANAGER
 
create 
	default_create
 
feature -- Access
 
	frozen last_exception: EXCEPTION
			-- Last exception
 
feature -- Status report
 
	is_caught (a_exception: TYPE [EXCEPTION]): BOOLEAN
			-- If set, type of `a_exception' is raised.
		ensure
			not_is_ignored: not is_ignored (a_exception)
 
	is_ignorable (a_exception: TYPE [EXCEPTION]): BOOLEAN
			-- If set, type of `a_exception' is ignorable.
 
	is_ignored (a_exception: TYPE [EXCEPTION]): BOOLEAN
			-- If set, type of `a_exception' is not raised.
		ensure
			not_is_caught: not is_caught (a_exception)
 
feature -- Status setting
 
	catch (a_exception: TYPE [EXCEPTION])
			-- Set type of `a_exception' is_ignored.
		require
			a_exception_not_void: a_exception /= Void
		ensure
			is_ignored: not is_ignored (a_exception)
 
	ignore (a_exception: TYPE [EXCEPTION])
			-- Make sure that any exception of code `code' will be
			-- ignored. This is not the default.
		require
			a_exception_not_void: a_exception /= Void
			is_ignorable: is_ignorable (a_exception)
		ensure
			is_caught: is_ignored (a_exception)
 
	set_is_ignored (a_exception: TYPE [EXCEPTION]; a_ignored: BOOLEAN)
			-- Set type of `a_exception' to be `a_ignored'.
		require
			a_exception_not_void: a_exception /= Void
			a_ignored_implies_is_ignorable: a_ignored implies is_ignorable (a_exception)
		ensure
			is_ignored_set: is_ignored (a_exception) = a_ignored
 
end -- class EXCEPTION_MANAGER

Default rescue

As specified in ECMA, `default_rescue' in ANY or its descendant is called as unqualified when an internal routine or an attributes with no rescue clause fails. We choose not to invoke it when it is not redefined, or this will be too expensive.

class ANY
...
    default_rescue
       do
       end
...
end

Sample code: Raising and catching developer exception

class
    MY_EXCEPTION
 
inherit
    DEVELOPER_EXCEPTION
 
end
class
    A
 
feature
 
    f
        local
            retried: BOOLEAN
        do
            if not retried then
                my_exception.raise   -- Raise user-defined exception.
            end
        rescue
            if last_exception = my_exception then   -- Check the exception object was the one user defined.
                print ("True" + "%N")
            else
                print ("False" + "%N")
            end
        end
 
    my_exception: MY_EXCEPTION
        once
            create Result
        end
 
    end
class
    APPLICATION
 
inherit
    A
 
create
    make
 
feature -- Initialization
 
    make
            -- Run application.
        local
            a: A
            retried: BOOLEAN
        do
            if not retried then
                create a
                a.f
            end
        rescue
            retried := True
 
            -- `last_exception' is an object of ROUTINE_FAILURE?
            if attached {ROUTINE_FAILURE} (create {EXCEPTION_MANAGER}).last_exception as l_exception then
                print ("True" + "%N")
            else
                print ("False" + "%N")
            end
 
            -- `original' exception is the one caused `last_exception'?
            if last_exception.original = my_exception then   
                print ("True" + "%N")
            else
                print ("False" + "%N")
            end
            retry
        end
 
end -- class APPLICATION