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