[comp.lang.modula3] Local exceptions

stolfi (Jorge Stolfi) (05/18/91)

I notice that Modula-3 allows exception declarations only at the top
level of modules and interfaces.

What was the point of this restriction?  (Local exceptions seem a pretty
natural concept.)

--j

  Jorge Stolfi
  DEC Systems Research Center
  stolfi@src.dec.com, ...!decwrl!stolfi
  

gnelson (Greg Nelson) (05/18/91)

Jorge Stolfi asks,

    I notice that Modula-3 allows exception declarations only at the top
    level of modules and interfaces.
    
    What was the point of this restriction?  (Local exceptions seem a pretty
    natural concept.)

You can make sense of local exceptions, but the benefits are not worth
the cost of the effort.  I suppose that with local exceptions, you
want the following block to be a checked runtime error:

    EXCEPTION E;
    BEGIN
      TRY 
        EXCEPTION E; BEGIN RAISE E END
      EXCEPT
        E: (*Skip*)
      END
    END

That is, you want the different exception declarations to produce
different exceptions.  Now consider:

    PROCEDURE P(b: BOOLEAN) = 
      EXCEPTION E;
      BEGIN
        TRY
          IF b THEN 
            P(NOT b)
          ELSE
            RAISE E
          END
        EXCEPT
          E: (* Skip *)
        END
      END P;

Do you want P(FALSE) to be an unhandled exception?  That is, do the
declarations of EXCEPTION E at the two levels of recursion introduce
different exceptions, or the same exception?  If they introduce the
same exception, then the procedure call behaves differently than
its body would; semanticists will not like this.  If they introduce
different exceptions, programmers might well be surprised.  The Modula-3
committee could not find any real use for local exceptions, and
therefore decided that the wisest choice was "none of the above".

stolfi (Jorge Stolfi) (05/20/91)

Greg, thanks for your reply.  

However, I must say I didn't understand your example:

    PROCEDURE P(b: BOOLEAN) = 
      EXCEPTION E;
      BEGIN
        TRY
          IF b THEN 
            P(NOT b)
          ELSE
            RAISE E
          END
        EXCEPT
          E: (* Skip *)
        END
      END P;
    
I don't see what is the problem here, since the exception E is always
caught in the same frame where it is raised.  

If we substitute the body of P for the call, we get something like

    PROCEDURE P(b: BOOLEAN) = 
      EXCEPTION E;
      BEGIN
        TRY
          IF b THEN 
            WITH b = NOT b DO
              EXCEPTION E;
              BEGIN
                TRY
                  IF b THEN 
                    P(NOT b)
                  ELSE
                    RAISE E
                  END
                EXCEPT
                  E: (* Skip *)
                END
              END P;
            END
          ELSE
            RAISE E
          END
        EXCEPT
          E: (* Skip *)
        END
      END P;

which doesn't seem to pose any problems of interpretation. 

Perhaps you meant to write

    PROCEDURE P(b: BOOLEAN) = 
      EXCEPTION E;
      BEGIN
        IF b THEN 
          TRY P(NOT b) EXCEPT E: (* Skip *) END
        ELSE
          RAISE E
        END
      END P;

However, in this case the natural interpretation is that the RAISE
E is an error, since (according to the Twelve Changes) P has
an empty RAISES clause, and hence a call to P cannot raise E.

Am I confused?

I am also quite surprised by your assertion that local exceptions have
no real use.  Sure, they are not strictly necessary; but the same
can be said of local variables, types, constants, and procedures,
which you agree are Very Good Things. 

A typical use of local exceptions would be

  PROCEDURE Foo(..) =

    EXCEPTION NotFound;
    PROCEDURE Lookup(...) RAISES {NotFound} =
      BEGIN
        ...
      END Lookup;

    BEGIN
      ...
      TRY Lookup(...) EXCEPT NotFound => ... END;
      ...
    END Foo;

--jorge

PS.  By the way, the argument that "a procedure call should have the same
effect as its body" is not very convincing. Consider for example

   PROCEDURE F(n: INTEGER): REFANY =
     TYPE T = BRANDED REF INTEGER;
     BEGIN
       IF n = 0 THEN 
         WITH t = NEW(T) DO t^ := 0; RETURN t END
       ELSE
         RETURN F(n-1)
       END
     END F;

From this code, I would expect F(0) and F(1) to have the same typecode
(and the SRC compiler seems to agree.)  However, if we substitute the
body of F for the call F(n-1), we will get two declarations of type
T, and the brands of F(0) and F(1) will be different.

gnelson (Greg Nelson) (05/21/91)

Jorge points out that I flubbed my example.  The question is, what
do you want to be the result of P(TRUE) after:

    PROCEDURE P(b: BOOLEAN) RAISES ANY = 
      EXCEPTION E;
      BEGIN
        IF b THEN 
          TRY P(NOT b) EXCEPT E: (* Skip *) END
        ELSE
          RAISE E
        END
      END P;

 

stolfi (Jorge Stolfi) (05/21/91)

    [Greg Nelson:] The question is, what do you want to be the result
    of P(TRUE) after:
    
        PROCEDURE P(b: BOOLEAN) RAISES ANY = 
          EXCEPTION E;
          BEGIN
            IF b THEN 
              TRY P(NOT b) EXCEPT E: (* Skip *) END
            ELSE
              RAISE E
            END
          END P;

I suppose the most useful interpretation is that each *static*
occurrence of "EXCEPTION Id" defines a new exception, irrespective
of how many times it is "executed".  Then all occurrences of E in the
above example refer to the same exception.

Admittedly this is not consistent with the principle of structural
type equivalence, but is at least consistent with the semantics of
BRANDED T.

By the way, here is another situation where the Report seems
a bit ambiguous about static versus dynamic scopes:

    MODULE M2 EXPORT Main;
    
      PROCEDURE MemeChose(n: INTEGER; p: PROCEDURE():INTEGER): BOOLEAN =
        
        PROCEDURE G(): INTEGER = 
          BEGIN RETURN n END G;
          
        BEGIN
          IF n = 0 THEN 
            RETURN p = G
          ELSE 
            RETURN MemeChose(n-1, G)
          END
        END F;

      PROCEDURE Irrelevant(): INTEGER = BEGIN RETURN 0 END P0;

    BEGIN
      <* ASSERT NOT MemeChose(1, Irrelevant) *>
    END M2.

Should the ASSERT fail or succeed?  

The question is whether all instances of G are the same procedural
value, i.e. whether the "environment" component of a procedure value
is a static scope or a dynamic scope.  To further confound the issue,
the Report calls G a procedure *constant*...

(The SRC compiler does the right thing, by the way.)

--jorge

stt@inmet.inmet.com (05/29/91)

Re: non-top-level exceptions
> /* Written 12:41 pm  May 21, 1991 by stolfi@src.dec.com */
> 
>     [Greg Nelson:] The question is, what do you want to be the result
>     of P(TRUE) after:
>     
>         PROCEDURE P(b: BOOLEAN) RAISES ANY = 
>           EXCEPTION E;
>           BEGIN
>             IF b THEN 
>               TRY P(NOT b) EXCEPT E: (* Skip *) END
>             ELSE
>               RAISE E
>             END
>           END P;
> 
> I suppose the most useful interpretation is that each *static*
> occurrence of "EXCEPTION Id" defines a new exception, irrespective
> of how many times it is "executed".  Then all occurrences of E in the
> above example refer to the same exception.
> 
> Admittedly this is not consistent with the principle of structural
> type equivalence, but is at least consistent with the semantics of
> BRANDED T.

Ada took this path to defining the meaning of exceptions declared
in a recursive procedure, namely that each "static" occurrence defines
a unique exception.  My recommendation:  Avoid the problem.  Stick
with top-level only.  It is of marginal use to allow local exceptions,
and the butchery done to the normal model of declarations is not worth
the trouble.

Furthermore, it really gets nasty with "generics" in Ada,
since each instantiation is considered a new "static" occurrence,
requiring replication of exceptions even if otherwise the code
can be shared between two generic instantiations.

S. Tucker Taft     stt@inmet.inmet.com
Intermetics, Inc.
Cambridge, MA  02138

stolfi (Jorge Stolfi) (05/30/91)

    > [S. Tucker Taft:] Ada took this path to defining the meaning of
    > exceptions declared in a recursive procedure, namely that each
    > "static" occurrence defines a unique exception.  My recommendation:
    > Avoid the problem.  Stick with top-level only.  It is of marginal
    > use to allow local exceptions, and the butchery done to the normal
    > model of declarations is not worth the trouble.

A strong argument for allowing local exceptions is precisely that they
would simplify the model of declarations.  If types, procedures,
constants, and variables can be declared at any level, why should
exceptions be an exception?

Besides, BRANDED types already follow the "static" declaration paradigm.

    > Furthermore, it really gets nasty with "generics" in Ada,
    > since each instantiation is considered a new "static" occurrence,
    > requiring replication of exceptions even if otherwise the code
    > can be shared between two generic instantiations.

I suppose this will be a problem also for Modula-3 generics, although
the way the latter are implemented (with explicit instantiations) makes
the non-equivalence of such exceptions more obvious and understandable.

  Jorge Stolfi
  DEC Systems Research Center

----------------------------------------------------------------------
DISCLAIMER: I'm only a user...