[comp.lang.eiffel] Exceptions and Assertions

rick@tetrauk.UUCP (Rick Jones) (09/03/90)

I apologise for the length of this posting, but I want to express my ideas
clearly as the basis for a discussion (I hope I've done so).  Please read on...


One of Eiffel's most powerful features is its exception handling, and in my
current work I am experimenting with a programming style which places heavy
reliance on this system.  The general concept is that _all_ "non-normal"
conditions are treated as exceptions, including those which may be expected to
occur quite frequently.

The application is interactive transaction processing, and this means that
there are a lot of non-normal conditions which can arise.  These can include
screen input validation errors, database record-not-found errors, invalid
transaction data, etc.  The rationale for this approach is that in conventional
programming methods, typically over 50% of the code is concerned with handling
such non-normal conditions.  If the program logic is reduced to what is
required if all input is correct and all conditions are as expected, a large
amount of complexity can be eliminated.

The Eiffel concept of raising an exception where it is detected and letting it
ripple back to a point where it can be handled offers a very attractive
solution to this problem.  This raises a philosophical issue:  Eiffel's
assertions are geared towards the concept of ensuring program correctness,
with just the "raise" statement available as an intended run-time exception
condition.  The concept presented by the manuals and the book "Object Oriented
Software Construction" is that no program should violate its assertions at
run-time;  if it does it is incorrect.

The problem is that assertions are a much clearer way of specifying what
conditions are required to exist than using raise.  E.g.

using "raise":

	if not condition_1 or not condition_2 or condition_3
	then
		raise ("failed")
	end

using an assertion clause:

	check
		need_cond_1: condition_1 ;
		need_cond_2: condition_2 ;
		not_cond_3: not condition_3 ;
	end

The assertion clause needs no IF logic and very little AND/OR logic, and
provides for an explicit label for each condition.  The conditions are also
stated positively (i.e. what is required) as opposed to negatively (i.e. what
will cause failure).  Using "raise" also requires a class to inherit from
EXCEPTIONS (this is only a minor inconvenience), but more significantly
assertions are an inherent part of the language, while "raise" is not.

A second problem is redundancy.  A feature may be written with a precondition
such as:

	doit (param:CLASS) is
	require
		not param.Void
	do
		...

A client of this feature should therefore test the proposed argument before
calling doit as in:

	(c is already declared as "c:CLASS")

	if not arg.Void
	then
		c.doit (arg)
	end

However, if a void arg is considered a non-normal condition, the general
requirement is to raise an exception.  This leads to:

	if not arg.Void
	then
		c.doit (arg)
	else
		raise ("arg void")
	end

This of course begs the question: why not leave the precondition checks active
and save the redundancy of checking for the same thing twice in different ways
and different places?  A precondition violation will effectively raise an
exception in the same place.

This starts to place a rather different emphasis on assertions, or at least
some of them.  I agree completely that invariants, postconditions, and loop
variants/invariants are issues of program correctness, since the principle of
contracting is that a routine _guarantees_ the postcondition and invariant if
the preconditions are met.  It is the preconditions which may arguably be used
as a run-time exception check rather than purely a correctness test.

Eiffel's other assertion construct is "check", and this is also quite
interesting.  The first example compares its use to "raise", where I suggested
it is more elegant.  I don't know to what extent Eiffel programmers use the
check construct in practice to ensure the correctness of their programs, but I
find it hard to envisage many circumstances where it would be really useful.
It seems naturally more suited to producing real run-time exceptions.  The
general situation is where a routine is called with valid preconditions, but
due to subsequent events not checkable at precondition time, is still unable to
meet its postcondition and invariant.

Run-time reliance on preconditions can be used in practice simply by ensuring
that all classes are compiled with at least precondition checking on.  As
standard, check assertions are not enabled in this mode, but I have argued
above that they should be.  It turns out that, since the assertion level which
is actually compiled is a C compile-time option controlled by macros in
"_eiffel.h", a small change to "_eiffel.h" enables check assertions whenever
precondition checking is on.  We are currently working in this mode in our
development environment.


Having presented this concept and the rationale behind it, I would like to know
how other Eiffel users, and those as ISE, feel about this as a style of
programming.  Some practical considerations are:

	Efficiency -	how does precondition checking throughout a program
			(which includes void reference checks on all feature
			calls) compare to programmed condition checks and calls
			to "raise"?

	Check -		to what extent does anyone use "check" assertions at
			present, and is it more appropriate that they be
			compiled in if precondition checking is enabled?

	Identity -	what is the best way for a rescue clause to analyse the
			exception which occured when there are a large number
			of possible exceptions (this applies to both assertions
			and use of "raise")?

	Specification -	how can a class interface specify clearly and reliably
			what exceptions may be raised apart from invariants
			and pre/post-conditions? (this seems a further argument
			in favour of check over raise, since such information
			could be extracted by "short")

Those who were present at Bertrand Meyer's presentation at TOOLS '90 on
concurrent and distributed processing support in Eiffel will recall that the
proposed extension required some change to the concept of precondition
checking, so that it did become a run-time issue.  How do the ideas above fit
into that scenario (perhaps this is one Bertrand should answer)?


I look forward to some constructive discussion ...

-- 
Rick Jones					You gotta stand for something
Tetra Ltd.  Maidenhead, Berks			Or you'll fall for anything
rick@tetrauk.uucp (...!ukc!tetrauk.uucp!rick)	     - John Cougar Mellencamp

bertrand@eiffel.UUCP (Bertrand Meyer) (09/05/90)

This is not an extensive answer, but a reaction to one specific
point of Rick Jones's posting. He notes that:

> A feature may be written with a precondition such as:
>  
> /1/        doit (param: CLASS) is
>                 require
>                      not param.Void
>                 do
>                      ...
> 
> A client of this feature should therefore test the proposed argument
> before calling doit, as in: (...)
>  
> /2/         if not arg.Void then c.doit (arg) end
> 
> (...)
> 
> This of course begs the question: why not leave the precondition checks
> activeand save the redundancy of checking for the same thing twice in
> different ways and different places? A precondition violation will
> effectively raise an exception in the same place.

A precondition is a requirement on clients, telling them what
they must achieve before a call if they want the call to execute
as advertized (i.e. ensure the postcondition on exit). 

A good client will ``do its homework'' before every call, that is
to say, ensure the precondition. But there is more than one
method to do this. /2/ is one such method; it is nice because of its
universality - it will work regardless of the precondition
(although it leaves open the question of what to do when the
precondition is not satisfied; in practice, the ``if'' will
usually need to have an ``else'', not shown in the above version).
But in many cases you will enforce the precondition by means other
than testing for it. In simple (but frequent) cases the precondition
is simply enforced by the context; a trivial example is the call

	p (x ^ 2 + y ^ 2)

where the precondition of p (a: REAL) is a >= 0.

To take a more common example, assume `l' is a list and you call `l.r',
where the precondition of `r' is that l be a non-empty list.
Assume further that the instruction executed just before the call
was was `l.insert (...)', where `insert' guarantees (as
part of its postcondition) that the list will be non-empty.
Then you do not need to use form /2/. One may go further and
state that you should not.

This is only a step towards answering Mr. Jones's broader question.
Should we tend towards a situation where precondition checking
will always be on, even in a released software product?
-- 
-- Bertrand Meyer
bertrand@eiffel.com

pgh@stl.stc.co.uk (P.G.Hamer) (09/06/90)

In article <404@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes:
>>This is only a step towards answering Mr. Jones's broader question.
>Should we tend towards a situation where precondition checking
>will always be on, even in a released software product?
>-- 
>-- Bertrand Meyer

Which raises the even broader question. Can at least some preconditions be
expressed so that they can be checked at compile time? In which case the 
there is zero run-time overhead, and *much more importantly* some classes
of error can be *guaranteed* not to occur (and hence there is no potential
exception to consider).

The language NIL claimed to support an interesting class of such compile
time checks using a concept called 'typestate'. 

Basically it added a form of context-sensitive type checking. A finite state
machine was defined for each object type giving: its major states; the
operations that were legitimate in each state; and the resultant state (or set
of possible states). Not only the type but the state of parameters is given. By
using data flow analysis it is possible to verify that an object can never have
an operation performed on it when it is in an unsuitable state.

For example, a stack might have three states; empty, full, and partially-full;
and pushing onto a full stack or popping from an empty one is (defined to be)
illigitimate. Programs which pushed onto a stack which was not demonstrably 
non-full (ie with a history of: just created and hence empty, just popped, just
tested for non-fullness, or an input with a stateset excluding full) would
generate compile time errors.

For details of what seems to be a very powerful idea  see -

Typestate: a programming language concept for enhanced software reliability.
R.E. Strom.
IEEE Trans S.E.
12(1), Jan 86, pp  157-171

NIL was (is?) an experimental language for distributed processing, with several
unusual features. Some of these undoubtably made typestate checking easier. I
don't how possible it would be to add it to a language such as Eiffel. 

Peter

cline@cheetah.ece.clarkson.edu (Marshall Cline) (09/07/90)

In article <3354@stl.stc.co.uk> pgh@stl.stc.co.uk (P.G.Hamer) writes:
>In article <404@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes:
>>Should we tend towards a situation where precondition checking
>>will always be on, even in a released software product?

>Which raises the even broader question. Can at least some preconditions be
>expressed so that they can be checked at compile time?

Doug Lea and I will be presenting a paper [1] at this year's SOOPPA
(I'm told SOOPPA is pronounced like a Long-Islander says `super' :-)
conference next week.  Our work is exactly related to this question.  We
support a rather strong set of `class axioms' which can either be turned
into runtime tests, some of which can hopefully be analyzed away by
increased compile-time analysis.

The syntax of our work is based on C++ (we call the system `A++' for
`Annotated C++'), but the basic ideas could be applied to any strongly
typed OOPL.  C++'s support for signature-conformal subclassing (`public')
and its separate support for non-sig-conformal subclassing (`private')
helps, but is not essential.

Another paper [2] which deals extensively with the present subject, will
be presented at the `C++ at Work' conference later this month.

The basic idea behind the scheme is to (conceptually) migrate
precondition testing out to the caller.  Thus a (I'll use C++ lingo)
member function `f()' on object `o' with precondition `pre' will
translate (conceptually) into:

		if (o.pre) o.f();
		else       throw exception;

The advantage to this is that simple (ha ha ha!) flow analysis can
eliminate some of these exception tests, without even the need for formal
verification techniques.  The problem is code bloat (there will in
general be more than one call to a function, so placing the precondition
test at the head of the function will mean less overall object code.

The (first) solution to the code bloat problem is a wrapper function.
Pretend we create an extra (hidden) member function `tested_f()' which
tests the preconditions then calls `f()'.  Then we still use a variant of
flow analysis (or formal verification if the user wants it) to decide
whether to call `o.f()' or `o.tested_f()'.

If the compiler's output is assembly language rather than `C', a slightly
more sophisticated solution is possible: alternate entry points:

		tested_f:
			...do the precondition tests...
		f:
			...do the regular function...
			ret

The problem with both these situations, is that `foreign languages' won't
know about this scheme, so their calls to `f()' will call the *UN*tested
version.  Soln: reverse the names:

		f:
			...do the precondition tests...
		raw_f:
			...do the regular function...
			ret

Other than the `raw' entry point (and the jmp around `raw_f's stack setup
after the precondition tests), this is *exactly* the same code generated
by current C++ compilers.  Thus A++ has the effect of allowing, on a CALL
BY CALL BASIS, some function calls to `jump around' the called function's
precondition tests.  The more compile-time analysis, the more calls that
can skip their precondition tests (we're not expecting that any piece of
`real' software will ever be able to eliminate *all* its precondition
tests -- we'd just like to eliminate the ones in low level loops, and we
believe this would be a big step forward).

The papers will be (please don't ask for preprints):

[1] Marshall Cline and Doug Lea, The Behavior of C++ Classes, to appear
in the Proceedings of the Symposium On Object-Oriented Programming
Emphasizing Practical Applications, Marist College, 1990.

[2] Marshall Cline and Doug Lea, Using Annotated C++, to appear in the
Proceedings of the 1990 C++ At Work Conference, 1990.

Marshall Cline

--
==============================================================================
Marshall Cline / Asst.Prof / ECE Dept / Clarkson Univ / Potsdam, NY 13676
cline@sun.soe.clarkson.edu / Bitnet:BH0W@CLUTX / uunet!clutx.clarkson.edu!bh0w
Voice: 315-268-3868 / Secretary: 315-268-6511 / FAX: 315-268-7600
Career search in progress; ECE faculty; research oriented; will send vita.
PS: If your company is interested in on-site C++/OOD training, drop me a line!
==============================================================================

richieb@bony1.uucp (Richard Bielak) (09/07/90)

I have two comments on recent postings, regarding exceptions and
assersion in Eiffel.

Speaking of exceptions, Rick Jones writes:

>One of Eiffel's most powerful features is its exception handling, and in my
>current work I am experimenting with a programming style which places heavy
>reliance on this system.  The general concept is that _all_ "non-normal"
>conditions are treated as exceptions, including those which may be expected to
>occur quite frequently.

IMHO, pushing non-normal cases into exception handlers is wrong. An
exception should only be used to handle the cases that were not
forseen by the developer. Doing a "raise" and handling a condition
elsewhere in the program has two problems:

1) Flow of the code is hidden from the programmer reading the code; a
"raise" is almost as bad as a GOTO.

2) If too much code is put in an exception handler, what will happen
if the handler encounters an exception? 


My other comment is on precondition checking. Preconditions are placed
in a routine to make it more re-usable. Some clients may do a extra
check, others may not. If some checks are occasionally duplicated -
well, that's the price you pay for re-usable software.

In his reply, Bertrand Meyer asks:

>Should we tend towards a situation where precondition checking
>will always be on, even in a released software product?

Yes! Yes! Yes! A million times yes! Let's use those MEGA-MIPS for
something useful. I don't believe we can ever write software that is
error free, we can at least write software that detects errors before
doing too much damage. Hardware detects errors, why not software?

...richie




-- 
+----------------------------------------------------------------------------+
|| Richie Bielak  (212)-815-3072      |  If it happens,                     ||
|| USENET:        richieb@bony.com    |      it is possible!                ||
+----------------------------------------------------------------------------+

davecb@yunexus.YorkU.CA (David Collier-Brown) (09/07/90)

Speaking of exceptions, Rick Jones writes:
| One of Eiffel's most powerful features is its exception handling, and in my
| current work I am experimenting with a programming style which places heavy
| reliance on this system.  The general concept is that _all_ "non-normal"
| conditions are treated as exceptions, including those which may be expected to
| occur quite frequently.

richieb@bony1.uucp (Richard Bielak) writes:
| IMHO, pushing non-normal cases into exception handlers is wrong. An
| exception should only be used to handle the cases that were not
| foreseen by the developer. Doing a "raise" and handling a condition
| elsewhere in the program has two problems:

| 1) Flow of the code is hidden from the programmer reading the code; a
| "raise" is almost as bad as a GOTO.

| 2) If too much code is put in an exception handler, what will happen
| if the handler encounters an exception? 

  We're really seeing a continuum here: the normal case, the general case
and the exceptional case.

  In the normal or common case, one wants to pass through a concise,
understandable sequence of code.  Often one wants this to be fast, in
some sense of the word.
  In the general case, one has to deal with the boundary conditions,
special oddities, etc, or the algorithm. Donald Knuth and PL/1 programmers
like this code to be physically but not logically separate.  Smalltalk
programmers like this to be both physically and logically separate and
"fault" into it.  Most other programmers write it as part of the common
case, often making it hard to understand.
  In the exceptional case, on has to deal with "can't happen" events.
Programmers with languages supporting exceptions make these exceptions,
and have them physically and logically separate.  People who don't have
exceptions fold them in with the general case...

   Exception-handlers are suitable for exceptions, but probably not
for the general-case, predictable boundary/oddball conditions. We
need a notation for the latter...
   As a palliative, I either use web and put them on a separate page, or
write little parameterless procedures that tag along with the parent
procedure.  The latter is an explicit kludge.


--dave
-- 
David Collier-Brown,  | davecb@Nexus.YorkU.CA, ...!yunexus!davecb or
72 Abitibi Ave.,      | {toronto area...}lethe!dave or just
Willowdale, Ontario,  | postmaster@{nexus.}yorku.ca
CANADA. 416-223-8968  | work phone (416) 736-5257 x 22075

sakkinen@tukki.jyu.fi (Markku Sakkinen) (09/10/90)

In article <14831@yunexus.YorkU.CA> davecb@yunexus.YorkU.CA (David Collier-Brown) writes:
>Speaking of exceptions, Rick Jones writes:
>| One of Eiffel's most powerful features is its exception handling, and in my
>| current work I am experimenting with a programming style which places heavy
>| reliance on this system.  The general concept is that _all_ "non-normal"
>| conditions are treated as exceptions, including those which may be expected to
>| occur quite frequently.
>
>richieb@bony1.uucp (Richard Bielak) writes:
>| IMHO, pushing non-normal cases into exception handlers is wrong. An
>| exception should only be used to handle the cases that were not
>| foreseen by the developer. Doing a "raise" and handling a condition
>| elsewhere in the program has two problems:
>| ...
>
>  We're really seeing a continuum here: the normal case, the general case
>and the exceptional case.
> ...
>
>   Exception-handlers are suitable for exceptions, but probably not
>for the general-case, predictable boundary/oddball conditions. We
>need a notation for the latter...

It seems to be in line with Bertrand Meyer's and Eiffel's attitudes
that exceptions are reserved for truly exceptional cases.
Probably there is no language that explicitly supports the kind
of three-case distinction that David Collier-Brown suggests.

Of course, language features can often be used in such ways or for
such purposes that the language's designers either have not imagined
or disapprove. My favourite example is Lynn Andrea Stein's OOPSLA'87
paper, "Delegation Is Inheritance", where she showed that Smalltalk-80
classes can be used as unique objects to provide the same kind of
delegation that is typical in classless OOPL's.

The extreme opposite to Eiffel w.r.t. exceptions is Modula-3,
in which exceptions are employed not only in "general" but also
in "normal" cases: an EXIT from a loop and a RETURN from a procedure
cause exceptions.

Markku Sakkinen
Department of Computer Science
University of Jyvaskyla (a's with umlauts)
Seminaarinkatu 15
SF-40100 Jyvaskyla (umlauts again)
Finland
          SAKKINEN@FINJYU.bitnet (alternative network address)

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

>Speaking of exceptions, I wrote:
>| One of Eiffel's most powerful features is its exception handling, and in my
>| current work I am experimenting with a programming style which places heavy
>| reliance on this system.  The general concept is that _all_ "non-normal"
>| conditions are treated as exceptions, including those which may be expected to
>| occur quite frequently.
>
>richieb@bony1.uucp (Richard Bielak) writes:
>| IMHO, pushing non-normal cases into exception handlers is wrong. An
>| exception should only be used to handle the cases that were not
>| foreseen by the developer. Doing a "raise" and handling a condition
>| elsewhere in the program has two problems:
>
>| 1) Flow of the code is hidden from the programmer reading the code; a
>| "raise" is almost as bad as a GOTO.

On the other hand, code which has to handle all the non-normal conditions
in-line also hides its flow.  Trying to find the salient thread among all the
actions dealing with the unexpected can be a real problem.  The advantage of
exceptions is that the routine which detects the problem may be several
nested layers removed from the routine which is capable of dealing sensibly
with it.  Without exceptions the intermediate code has to know about the error
conditions simply to pass them back to the previous level.

I look at exceptions as "structured goto's", and I don't think that makes them
bad per-se.  I know the NO-GOTO syndrome is a religion with some people, but
every construct can be valuable in the right place.  Basically, GOTO's can be
good as a way OUT, it's if you use them as a way IN that you get into trouble.

>| 2) If too much code is put in an exception handler, what will happen
>| if the handler encounters an exception? 

I agree here, exception handlers should be simple.  But Eiffel's "retry"
concept means that the exception handler (rescue clause) simply flags that an
exception has occurred, and the main procedure checks this flag.  The "real"
code which progresses the application following an exception is an alternative
route through the procedure body.  This is a unique (I think!) feature of
Eiffel's exception mechanism.

>| [paraphrased]: should Eiffel programs generally be run with pre-condition
>| checking on? - yes! yes! yes!

I am coming round more and more to this point of view.  I have done a few more
experiments on compilation options, and the overhead of running with
pre-condition checks on in the current implementation seems to more associated
with exception history tracing than with the assertion checks.  This is because
every routine gets a default rescue clause even if there is no explicit one.
This only disappears when the "no-assertion-check" compile option is used.

The most effective run-time (i.e. C compile-time) option would be to check
pre-conditions, void references, "check" clauses (my preference), but omit
history tracing.  This means that spurious setjmp() calls are avoided, and a
longjmp() in the case of an exception will go direct to the currently effective
rescue handler.  This all seems possible with a little re-arrangement of
the _eiffel.h file;  any comments from ISE, please?

davecb@yunexus.YorkU.CA (David Collier-Brown) writes:

>  We're really seeing a continuum here: the normal case, the general case
>and the exceptional case.
>
>  In the normal or common case, one wants to pass through a concise,
>understandable sequence of code.  Often one wants this to be fast, in
>some sense of the word.
>  In the general case, one has to deal with the boundary conditions,
>special oddities, etc, or the algorithm.
> ...
>  In the exceptional case, on has to deal with "can't happen" events.

I think this is fair comment.  It's clear that conventional code is for the
normal case, and exception handlers are for the exception case.  The question
seems to be, where does the general case belong?  Should the language have a
method for handling these conditions distinct from the other 2 cases?
If not (which is the current reality), I think it depends on the particular
application.  As I said, my application is transaction processing, and the
general style here is that an inability to complete the transaction requires
the entire transaction to be aborted.  The transaction manager is external to
Eiffel, and its ABORT routine is in effect an exception handler since it will
throw away everything done so far and return to its caller.  There seems to be
some merit in defining an Eiffel rescue clause at the same level as the
transaction start point whose job is to invoke the ABORT routine.  Hence any
inability to complete the transaction which may result from conditions only
encountered at some depth of routine calls can just raise an exception and
forget the problem.

However, this may not be an appropriate style for all applications.  In many
programs the general case may well be better handled in-line.


On this topic, I have just read the descriptions of the proposed C++ exception
handler in the latest issue of JOOP.  While it is far more limited than
Eiffel's exception handler, it does raise (?!?) some interesting points.

a.	It argues that program clarity is _improved_ if exceptions are used to
	handle the abnormal, which includes "expected errors" such as open-file
	errors.  This is in line with my arguments above.

b.	It provides a powerful means for the programmer to distinguish his own
	exceptions, since an object of a programmer-defined class is passed
	from the the exception point to the handler.

I think this second issue is a major problem in Eiffel, since there is only one
"programmer exception" type, and the only information which can be defined is a
string.  Assertions of course allow labels, but that comes to the same thing.
This is fine if you just want to print out the exception to the user, but if an
exception handler needs to do any serious analysis of the exception for the
purpose of alternative recovery strategies this is very difficult to do in a
structured manner.

Could Eiffel's exception mechanism be extended to allow more arbitrary
information to be passed to a rescue clause?
-- 
Rick Jones			Nothing ever happens, nothing happens at all
Tetra Ltd.			The needle returns to the start of the song
Maidenhead, Berks, UK		And we all sing along like before
rick@tetrauk.uucp						- Del Amitri

rnews@qut.edu.au (09/11/90)

 
> In his reply, Bertrand Meyer asks:
> 
>>Should we tend towards a situation where precondition checking
>>will always be on, even in a released software product?
> 
> Yes! Yes! Yes! A million times yes! Let's use those MEGA-MIPS for
> something useful. I don't believe we can ever write software that is
> error free, we can at least write software that detects errors before
> doing too much damage. Hardware detects errors, why not software?

From the theoretical view point I agree that we should always test
preconditions (and preferably postconditions and all assertions).
From the practioners view point I disagree.  Software is getting
ever more complex (not including assertion testing already) and
there is a ever growing need for faster hardware to allow software
to execute at an usable speed.  As running Eiffel programs with
full assertion testing shows, assertion testing is a very heavy
overhead, and is unacceptable in most commercial applications.
Always testing assertions at runtime can only be done when we have
an efficient method of checking assertions (having a separate processor
and process for testing pre/post conditions would be an interesting
project.

Au revoir,

@~~Richard Thomas  aka. The AppleByter  --  The Misplaced Canadian~~~~~~~~~~~@
{ InterNet: R_Thomas@qut.edu.au           ACSNet:  richard@earth.qitcs.oz.au }
{ PSI:      PSI%505272223015::R_Thomas                                       }
@~~~~~School of Computing Science - Queensland University of Technology~~~~~~@

pierson@encore.com (Dan L. Pierson) (09/13/90)

In article <1990Sep10.073627.17213@tukki.jyu.fi> sakkinen@tukki.jyu.fi (Markku Sakkinen) writes:

   The extreme opposite to Eiffel w.r.t. exceptions is Modula-3,
   in which exceptions are employed not only in "general" but also
   in "normal" cases: an EXIT from a loop and a RETURN from a procedure
   cause exceptions.

Not exactly.  EXIT and RETURN are defined in terms of exceptions in
order to clearly and concisely specify their interaction with other
(real) exceptions.  They come complete with restrictions which
guarantee that a compiler can produce "normal" (i.e. similar to C)
code for them.  This is explained in the second paragraph under EXIT
(top of page 23) of the revised report.
--

                                            dan

In real life: Dan Pierson, Encore Computer Corporation, Research
UUCP: {talcott,linus,necis,decvax}!encore!pierson
Internet: pierson@encore.com

kimr@eiffel.UUCP (Kim Rochat) (09/22/90)

In article <731@tetrauk.UUCP>, rick@tetrauk.UUCP (Rick Jones) writes:
> >| [paraphrased]: should Eiffel programs generally be run with pre-condition
> >| checking on? - yes! yes! yes!
> 
> 
> I have done a few more experiments on compilation options, and the
> overhead of running with pre-condition checks on in the current
> implementation seems to more associated with exception history tracing
> than with the assertion checks.  This is because every routine gets a
> default rescue clause even if there is no explicit one.  This only
> disappears when the "no-assertion-check" compile option is used.
> 
> The most effective run-time (i.e. C compile-time) option would be to
> check pre-conditions, void references, "check" clauses (my preference),
> but omit history tracing.  This means that spurious setjmp() calls are
> avoided, and a longjmp() in the case of an exception will go direct to
> the currently effective rescue handler.  This all seems possible with a
> little re-arrangement of the _eiffel.h file;  any comments from ISE,
> please?

I'd like to thank Rick for this suggestion.  We've been trying to
figure out a way to reduce the cost of assertion monitoring, but had
been assuming that a full exception history trace was a requirement.
If you are willing to do without the full history trace, this message
contains a work-around which reduces the time to execute a program
which monitors assertions by a factor of 10.

The context diffs at the end of this article are intended to be used
with the 'patch' program to modify Eiffel/files/_eiffel.h for Eiffel
2.2B.

The modified _eiffel.h works as usual unless your 'C_COMPILER'
environment variable (See section 4.17 of the Environment manual)
or the makefile 'CC' macro in a C package defines -DNOSTACKTRACE.

When -DNOSTACKTRACE is used in conjunction with PRECONDITIONS or
ALL_ASSERTIONS, the appropriate assertions are monitored.  If an
assertion fails, only the routine raising the exception plus those
routines having rescue clauses will be shown in the exception history.

This behavior results from only calling setjmp to register a rescue
clause when a routine actually has a rescue clause (or a class rescue)
which must be registered, and not registering routines with default
rescue clauses.

The resulting program monitors the appropriate assertions, and executes
in approximately 1/10 the time. If an assertion fails, you will usually
see a single exception history entry like the following:

System execution failed.
Below is the sequence of recorded exceptions:
--------------------------------------------------------------------------------
Object Class        Routine              Nature of exception              Effect
--------------------------------------------------------------------------------
2EB3C  LIST         put                  "index_small_enough":
					 Precondition violated.           Fail
-------------------------------------------------------------------------------

Usage Information:

(Note: STACKTRACE is used below to indicate the absence of
the -DNOSTACKTRACE option from the C_COMPILER environment variable)

1) This modification has been tested for Eiffel version 2.2B on
   a Sun-3.  It is provided for those customers who want to run with 
   assertions on and not pay the overhead associated with maintaining
   the full exception history stack.  While it is believed to work, 
   this modification is not yet supported by Interactive.  The user should 
   understand the Eiffel compilation and configuration management model
   before trying to use -DNOSTACKTRACE, in particular the locations 
   and names of object files corresponding to the CHECK1 and CHECK2
   compilation options.  (If you don't know what I'm talking about,
   you probably shouldn't try to use this).

2) There is a minor semantic difference between running in -DNOSTACKTRACE
   and normal modes.  With -DNOSTACKTRACE, if a routine which has a
   precondition failure also has a rescue clause (or is in a class
   having a class rescue clause), the exception will be raised in the
   routine containing the failed precondition, as opposed to the normal
   condition where the exception is raised in the caller of the routine.

   This occurs because a failed precondition raises the exception by
   doing a longjmp to the top entry on the exception stack.  In the
   STACKTRACE case, the top stack entry is ignored since it is assumed
   to be the rescue clause of the current routine, default or real.

   In the NOSTACKTRACE case, the routine knows that the topmost entry
   is a valid rescue clause, but doesn't know who registered it.  An
   easy way to fix this would be to register the rescue clause for a
   routine AFTER checking the precondition, which would require 
   rearranging the generated code.

3) Since NOSTACKTRACE is not an SDF option, 'es' doesn't keep
   track of which classes have been compiled with NOSTACKTRACE.
   You must be careful to understand what you are doing if you use
   NOSTACKTRACE.

   Because of the difference in the handling of the top exception stack
   entry in the case of a precondition failure (ignored by STACKTRACE,
   used by -DNOSTACKTRACE), it is important that classes compiled with
   -DNOSTACKTRACE not call classes compiled with STACKTRACE.  If this
   occurs, a valid rescue clause will be ignored. 
   
   If the top exception stack entry is for the system exception handler
   and it is ignored, a serious system error will result if an
   exception occurs.  On my MIPS, I see 'longjmp botch' followed by
   'exception occurred in exception handler'.  An easy way to protect
   against this happening is to put an empty rescue clause in the Create
   routine of your root class to register a second rescue clause in the
   exception stack.

   It is permissible for classes compiled with STACKTRACE to call
   classes compiled with -DNOSTACKTRACE.  This will work just as
   usual, with a full exception history trace produced for those 
   classes compiled with STACKTRACE.

   A convenient way of using -DNOSTACKTRACE would be to compile the
   libraries with -DNOSTACKTRACE, and your application with STACKTRACE.
   The (hopefully large) percentage of the time spent in library
   routines won't incur any overhead, but if your application causes
   a precondition failure in a library routine, you will get a
   stack trace indicating which library routine and precondition
   failed, followed by a trace of your application up to the point
   where it called the library routine.

4) If you want to switch STACKTRACE on or off, you will need to remove
   all object files compiled with the opposite sense and rebuild your
   application.  These object files can be found in each cluster directory
   in the files <class>.E/*C1.o (for PRECONDITIONS) and <class>.E/*C2.o 
   (for ALL_ASSERTIONS).  Don't forget the KERNEL cluster, which isn't
   normally listed in the SDF.

   An easier way of switching back and forth would be to use the
   OPTIMIZE SDF option.  If all classes compiled with OPTIMIZE and
   PRECONDITIONS (or ALL_ASSERTIONS) are compiled with -DNOSTACKTRACE,
   and all classes compiled without OPTIMIZE (but with PRECONDITIONS
   or ALL_ASSERTIONS) are compiled without STACKTRACE, then you can
   switch between STACKTRACE and NOSTACKTRACE simply by changing the
   OPTIMIZE option in the SDF.  I suggest using OPTIMIZE with
   -DNOSTACKTRACE because it doesn't make much sense to optimize classes
   which do all those setjmp calls.

5) If you want to isolate the object files compiled with -DNOSTACKTRACE
   from other users sharing library or application classes with you,
   you can make a c_package of your application and modify the makefile
   to add -DNOSTACKTRACE to the 'CC' macro.  Remember to remove all
   object files before recompiling.

6) Just a reminder that -DNOSTACKTRACE doesn't make any difference if
   used  with NO_ASSERTION_CHECK.

7) This is only one of several possible solutions for improving the
   performance of assertion monitoring.  After trying this out, please 
   let us know how important it is to maintain the full exception history 
   mechanism in conjunction with faster assertion checking.  (That is, 
   should ISE provide a switch in the .eiffel file to turn off the full 
   exception history, or should ISE work on keeping the full exception 
   history but making it faster?)
   
Kim Rochat
Responses to: eiffel@eiffel.com

-------------------------------------------------------------------------------
*** Eiffel/files/_eiffel.h.orig	Wed Nov 29 11:37:32 1989
--- Eiffel/files/_eiffel.h	Mon Sep 17 19:17:51 1990
***************
*** 12,19 ****
--- 12,21 ----
  #ifdef CHECK1
  #define REQUIRE
  #define TESTVOID
+ #ifndef NOSTACKTRACE
  #define FULLEXCEPT
  #endif
+ #endif
  #ifdef CHECK2
  #define REQUIRE
  #define TESTVOID
***************
*** 22,29 ****
--- 24,33 ----
  #define INVARIANT
  #define VARIANT
  #define CHECK
+ #ifndef NOSTACKTRACE
  #define FULLEXCEPT
  #endif
+ #endif
  #define EB_ASSER_START -1
  #define E_VOID -1
  #define E_REQUIRE -2
***************
*** 56,64 ****
--- 60,76 ----
  #define PROJMP12 PROJMP012
  #endif
  #ifdef  CHECK2
+ #ifdef  FULLEXCEPT
  #define ERRJMP12 ERRJMP012
+ #else
+ #define ERRJMP12 _longjmp (*(jmp_stack+jmp_level-1), 1)
+ #endif
  #define PROJMP12 PROJMP012
  #endif
+ #ifndef NOSTACKTRACE
+ #define ERRJMP12 ERRJMP012
+ #define PROJMP12 PROJMP012
+ #endif
  #ifndef  ERRJMP12
  #define ERRJMP12
  #define PROJMP12
***************
*** 72,83 ****
  #endif
  #define VIOLAT012 violated (DT[class], routine, last_exception, e_tag, BCurrent)
  #define SETRES012 set_name_except (DT[class], routine, BCurrent)
! #ifdef  FULLEXCEPT
  #define VIOLAT12 VIOLAT012
  #define SETRES12 SETRES012
  #else
- #define VIOLAT12 
  #define SETRES12 
  #endif
  #define RETRY TRACERET;if (dc_unrolled!=0) VIOLAT012; retry (DT[class], routine, BCurrent);goto start
  #ifdef TRACE
--- 84,100 ----
  #endif
  #define VIOLAT012 violated (DT[class], routine, last_exception, e_tag, BCurrent)
  #define SETRES012 set_name_except (DT[class], routine, BCurrent)
! #ifdef CHECK2 
! #define SETRES12 SETRES012
  #define VIOLAT12 VIOLAT012
+ #else
+ #ifdef FULLEXCEPT 
  #define SETRES12 SETRES012
+ #define VIOLAT12 VIOLAT012
  #else
  #define SETRES12 
+ #define VIOLAT12 
+ #endif
  #endif
  #define RETRY TRACERET;if (dc_unrolled!=0) VIOLAT012; retry (DT[class], routine, BCurrent);goto start
  #ifdef TRACE

jimp@cognos.UUCP (Jim Patterson) (09/25/90)

In article <410@eiffel.UUCP> kimr@eiffel.UUCP (Kim Rochat) writes:
>In article <731@tetrauk.UUCP>, rick@tetrauk.UUCP (Rick Jones) writes:
 >> >| [paraphrased]: should Eiffel programs generally be run with pre-condition
 >> >| checking on? - yes! yes! yes!
 >> 
 >> 
 >> The most effective run-time (i.e. C compile-time) option would be to
 >> check pre-conditions, void references, "check" clauses (my preference),
 >> but omit history tracing.  This means that spurious setjmp() calls are
 >> avoided, and a longjmp() in the case of an exception will go direct to
 >> the currently effective rescue handler.  This all seems possible with a
 >> little re-arrangement of the _eiffel.h file;  any comments from ISE,
 >> please?
>
>I'd like to thank Rick for this suggestion.  We've been trying to
>figure out a way to reduce the cost of assertion monitoring, but had
>been assuming that a full exception history trace was a requirement.
>If you are willing to do without the full history trace, this message
>contains a work-around which reduces the time to execute a program
>which monitors assertions by a factor of 10.

There's no reason that setjmp() has to be used to implement an
exception history trace. A small amount of inline setup code in each
function is sufficient to provide a trace history, and gives a
significant performance boost over setjmp (which is a very expensive
library call as such things go).

The basic technique is to allocate an Eiffel "call frame" in each
function which contains the class and routine numbers and a back link
to the previous function. The setup logic initializes the call frame
and links it to the previous frame; exit code unlinks (pops off) the
call frame.

This does require changing the Eiffel compiler and runtime, however,
so isn't as simple as just modifying the _eiffel.h header.

-- 
Jim Patterson                              Cognos Incorporated
UUCP:uunet!mitel!cunews!cognos!jimp        P.O. BOX 9707    
PHONE:(613)738-1440                        3755 Riverside Drive
                                           Ottawa, Ont  K1G 3Z4