[Oberon] Serious type loophole in type case statements and a possible fix

Andreas Pirklbauer andreas_pirklbauer at yahoo.com
Tue Nov 3 06:53:19 CET 2020


Several people have asked (via email) why the following restrictions 

    1. The case variable must be either a local variable or value parameter of
       pointer type (pointing to a record) or a variable parameter of record type.

    2. Assignments to the case variable are not allowed in the statements Si.
       However, individual elements (record fields) of case variables may be modified.

    3. A case variable of pointer type cannot be passed as a variable parameter to a procedure.

have been proposed for the type case statement (=WITH statement in original Oberon-2)

   CASE p OF
     T1: S1 |
     T2: S2
   END ;

By design, this choice makes it *impossible* to alter a case variable p of a type case statement
within the scope of the CASE p OF … END block, and there cannot ever be any type loopholes.
 
See references [1] and [2] and the example in the appendix below.

It has been noted that this choice stands in contrast with e.g. the "Oakwood guidelines for Oberon-2",

    http://www.edm2.com/index.php/The_Oakwood_Guidelines_for_Oberon-2_Compiler_Developers

where it is possible to "alter a guarded pointer variable within the scope of a guarding WITH statement”.

* * *

The rationale for *not* adopting the Oakwood guidelines, but rather choosing the
more restrictive approach outlined above was the following:

a. If one allowed changing the case variable p of a type case statement *within* the scope
   of the case statement block, then the compiler would have to insert additional hidden
   type guards p(T) in the statements of the case statement. This constitutes a “hidden
   mechanism”, which seems to be "against the spirit of Oberon”.

b. Inserting hidden type guards would also negatively impact performance. Of course, a
   sophisticated compiler may be able to “detect” (at least in some cases) whether a type
   guard is actually needed, but in general this is not possible (for example if a global
   variable is passed as a parameter to a procedure containing a type case statement).

c. In the entire Oberon system (I am referring to Project Oberon 2013), there is not a
   single module, where the above restrictions cause a problem. Not one. In other words,
   these restrictions are not “real” restrictions, but represent good programming style.

d. If one *really* wants to change the value or type of variable p, one can always use
   the construct of a type test (using IS) instead, and insert type tests explicitly. This is
   not a bad, but a good thing, as it trains the programmer to use *explicit* type guards
   when needed, while allowing him/her to omit them when they are not needed.

      IF p IS T1 THEN p(T1).x := ..
      ELSIF p IS T2 THEN p(T2).y := 
      END

-ap

References:

[1] https://github.com/andreaspirklbauer/Oberon-extended/blob/master/Documentation/The-Revised-Oberon2-Programming-Language.pdf

[2] https://github.com/andreaspirklbauer/Oberon-type-case-statement-without-loopholes

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

APPENDIX: Test program

MODULE TestTypeCase;  (*test type case statements with restrictions*)

  TYPE R0 = RECORD fld0: INTEGER END ;
    R1 = RECORD (R0) fld1: INTEGER END ;
    R2 = RECORD (R0) fld2: INTEGER END ;

    P0 = POINTER TO R0;
    P1 = POINTER TO R1;
    P2 = POINTER TO R2;

  VAR p0: P0; p1: P1; p2: P2;
    r0: R0; r1: R1; r2: R2;

  PROCEDURE AssignGlobal();
  BEGIN p0 := p2
  END AssignGlobal;

  PROCEDURE Proc1(p0: P0); BEGIN END Proc1;
  PROCEDURE Proc2(VAR p1: P1); BEGIN END Proc2;
  PROCEDURE Proc3(VAR r0: R0); BEGIN r0 := r2 (*projection*) END Proc3;
  PROCEDURE Proc4(r0: R0); BEGIN END Proc4;
  PROCEDURE Proc5(VAR i: INTEGER); BEGIN END Proc5;
  PROCEDURE Proc6(b: BOOLEAN); BEGIN END Proc6;

  PROCEDURE check0();
  BEGIN p0 := p1;
    CASE p0 OF  (*ERROR "invalid case variable" - global case variables are not allowed*)
      P1:
        AssignGlobal();  (*if p0 were allowed to be global, the dynamic type of p0 would be set to P2 here*)
        p0.fld1 := 123  (*and there would be a type violation here, as p0 is considered of type P1*)
    END
  END check0;

  PROCEDURE check1();
    VAR p, pa: P0;
  BEGIN p := p1;
    CASE p OF
      P1:
        pa := p;
        Proc1(p);
        Proc2(p);  (*ERROR "invalid parameter" - cannot pass a case variable of a pointer type as a VAR parameter*)
        Proc5(p.fld1);
        p := p2;  (*ERROR "invalid assignment"  - cannot modify a case variable through an assignment*)
        p.fld1 := 123;
        CASE p OF
          P1:
            p := p1;  (*ERROR "invalid assignment" - cannot modify a case variable through an assignment*)
            Proc1(p);
            Proc2(p)   (*ERROR "invalid parameter" - cannot pass a case variable of a pointer type as a VAR parameter*)
        END ;
        p := p1  (*ERROR "invalid assignment" - cannot modify a case variable through an assignment*)
    END
  END check1;

  PROCEDURE check2();
  BEGIN p0 := p1;
    IF p0 IS P1 THEN
      AssignGlobal();  (*the runtime type of p0 is not P1 anymore, but P2, after this statement*)
      p0(P1).fld1 := 123;  (*thereore, there (correctly) will be a type guard failure at runtime here*)
    END
  END check2;

  PROCEDURE check3(VAR r: R0);
    VAR rr: R1;
  BEGIN r := r1; (*projection of the fields of R1 onto the fiels of R0*)
    CASE r OF
      R1:
        rr := r;
        r := r1;  (*ERROR "invalid assignment" - cannot modify a case variable through an assignment*)
        Proc3(r);
        Proc4(r);
        r.fld1 := 234
    END
  END check3;

  PROCEDURE check4();
    VAR r: R1;
  BEGIN r := r1;
    CASE r OF  (*ERROR "invalid case variable" - a record must be a variable parameter*)
      R1: r.fld1 := 345
    END
  END check4;

  PROCEDURE check5(p: P0);
  BEGIN
    CASE p OF
      P1: Proc6(p IS P1)
    END
  END check5;

  PROCEDURE check6(VAR p: P0);
  BEGIN
    CASE p OF P1:  (*ERROR "invalid case variable" - a pointer must be either a local variable or a value parameter*)
      p0 := p2;
      p.fld1 := {4,1}
    END
  END check6;

  PROCEDURE Go*;
  BEGIN
    check0();
    check1();
    check2();
    check3(r0);
    check4();
    check5(p0);
    p0 := p1; check6(p0)
  END Go;

BEGIN NEW(p0); NEW(p1); NEW(p2)
END TestTypeCase.



More information about the Oberon mailing list