The ModulaTor logo 7KB

The ModulaTor

Oberon-2 and Modula-2 Technical Publication

Erlangen's First Independent Modula_2 Journal! Nr. 4/May-1993

How to turn a great separation into a better partnership

or

A modest proposal for an object oriented extension of ISO Modula-2

by Guenter Dotzel, ModulaWare

Draft version, 29-Apr-1993 for The ModulaTor, Vol 3, Nr. 4. 2nd corrected ed., 04-May-1993, 3rd ed. 14-May-1993.


0. Contents
1. Introduction
2. Motivation
3. OO-model type
3.1. Summary of language changes and extensions
4. Declarations and scope rules
5. Constant declarations
6. Type declarations
6.1. Record types
6.2. Pointer types
6.3. Procedure types
6.4. Variable declarations
7. Expressions
7.1. Operands
7.2. Relations
8. Statements
8.1. With-type statement
9. Procedure declarations
9.1. Type-bound procedures
9.2. Predeclared procedures
10. Class Modules
11. Required separate modules
11.1. Module ObjStorage
11.2. Module Objects_Types
12. Symbol file and definition module generation
13. Example: a generic table handler
- CLASS DEFINITION MODULE OAVL 
- CLASS MODULE USE_OAVL 
- CLASS MODULE OAVL 
14. Summary
Appendix A: Definition of terms 
1. Introduction

The German Modula-2 OO-extension working group of sc22.wg13 (DIN-AK) had a meeting, in Munich, 24-Apr-1993. Christof Brass, Markus Klingspor, Albert Wiedemann, Elmar Henne, and Guenter Dotzel attended this meeting.

Albert had prepared three concrete examples to illustrate three possible OO-extension proposals:

(1) The first example tried to combine tradition with new requirements for exported/not- exported, visible/unvisibile, private/hidden/public, accessible/unaccessible attributes and methods by marking those not accessible by clients as INTERNal. He proposed a new type CLASS for reference-based objects and a keyword METHOD. Explicit receiver specification is not necessary since method are declared within their classes in definition modules. In implementation, method names are qualified with the class type name. Inheritance is indicated using the Oberon-2 style syntax for record extension and redefined methods were marked with OVERRIDE.

(2) The second example introduced local modules of type CLASS for both, definition and implementation modules. Such class modules contain an EXPORT clause which defines what is visible outside the scope of the class module. Class modules INHERIT from other class modules. A new keyword METHOD is not required, since within class modules all procedures are automatically methods with an implicit receiver of the class module type. Different strategies for what is visible, public, accessible, inheritable in the different module kinds were proposed.

(3) The third example extended the second one by multiple inheritance and initialization and finalization bodies for class modules which could serve as constructors and destructors.

The working group proposed, discussed and rejected different strategies and implications of having separate definition and implementation modules containing classes. I followed this discussion for two hours and finally convinced myself that we need to make a step forward in the direction of Oberon-2 in that the definition and implementation modules have to be combined for the new type of so-called class modules.

Albert was not in favour of my idea. He argued that this is not Modula-2 style or tradition. He also felt that the separation is needed in order to tackle large programming projects. My experiences with Oberon-2 do not confirm Albert's critique. Why shouldn't we move on and inherit the experiences made with Oberon and Oberon-2 in ISO Modula-2 if they would drastically simplify the changes required for the OOP-extension.

2. Motivation

The separation between definition and implementation module forbids a simple integration of OOP-facilities in ISO Modula-2. With or without multiple inheritance, staying with the tradition would result in a language with many special rules which are difficult to explain and compilers which are difficult to implement.

To avoid most of the problems, I propose that the compiler instead of the programmer generates the definition for modules of type class. The resulting class definition module serves class module interface specification and documentation purposes only; it is not further processed. A new class definition module is generated in two cases:

(1) for new class modules and

(2) each time interface related parts are changed.

In both cases a new symbol file is generated too (see Chapter 12).

Optionally, in order to get descriptive comments into the class definition modules, special documentation directives could be used which are selectively copied from the class module source code to its class definition by the compiler.

The symbol files generated for class modules can be imported in any module be it class, definition, implementation or proper modules. Everything that can be done with traditional modules can then also be done with class modules. For non-OOP designs, it would be the programmer's choice whether he uses class modules in order to save time in module maintenance.

You might argue that not having the separation of definition and implementation is not common sense in Modula-2. But we still have definition modules, although generated automatically.

One reason for not having the separation is that with public and private attributes/methods, the symbol file must also contain information which is to be derived from the implementation module. Another strong point against the traditional separation is that visibility and accessibility problems with inheritance are significantly reduced.

Also the gap between accessible, visible and private/public in the proposals (1), (2), and (3) above is closed. All exported objects are marked textually at their declaration. Marking non-(visible, accessible, public) attributes as intern could later turn out as a security hole. One could simply forget to restrict the visibility of a field in the interface and everything works great until an unauthorized access happens. On the other hand, if an export-mark was forgotten, this would be recognized when a client module needs access to it. Consider the interface of an amplifier: if you want more bass, you do not turn-off treble.

The reason for having an explicit receiver specification in method declarations is contained in M~ossenb~ock's article Differences between Oberon and Oberon-2, ETH-Report Nr. 160, May-1991, revised May-1992. For those not familiar with Oberon-2, I quote the most important parts of M~ossenb~ock's article:

"We refrained from introducing the concept of a class but rather replaced it by the well-known concept of records. Classes are simply record types with procedures bound to them.

We also refrained from duplicating the headers of bound procedures in the record as it is done in other object-oriented languages like C++ or Object Pascal. This keeps record declarations short and avoids unpleasant redundancy (changes to a header would have to be made at two places in the program and the compiler would have to check the equality of the headers). If the programmer wants to see the record together with all procedures bound to it he uses a tool (a browser) to obtain the information on screen or on paper.

The procedures bound to a type may be declared in any order. They can even be mixed with procedures bound to a different type. In Object Oberon, where all methods have to be declared within their class declaration, it turned out that indirect recursion between methods of different classes make awkward forward declarations of whole classes necessary.

In languages like Object Pascal or C++, instance variables of the receiver object self can be accessed with or without qualification (i.e. one can write either x or self.x). In these languages it is sometimes difficult to see whether a name is an ordinary variable or an instance variable. It is even more confusing if the name denotes an instance variable that is inherited from a base class. We therefore decided that instance variables must always be qualified in Oberon-2. This avoids having a choice between two semantically equivalent constructs, which we consider undesirable in programming languages.

In Oberon-2, the receiver is an explicit parameter, so the programmer can choose a meaningful name for it, which is usually more expressive than the predeclared name self that is used in other object-oriented languages. The explicit declaration of the receiver makes clear that the object to which an operation is applied is passed as a parameter to that operation. This is usually not expressed in other object-oriented languages. It is in the spirit of Oberon to avoid hidden mechanisms.

In Object Oberon methods have the same syntax as ordinary procedures. In large classes where the class header is not visible near the method header it is impossible to see whether the procedure is an ordinary procedure or a method, and to which class the method belongs. In Oberon-2, the type of the receiver parameter of a bound procedure denotes the type to which the procedure is bound, so no confusion can arise."

3. OO-model type

The modest OO-model proposed in the following chapters is adopted from Oberon-2. It features

r single inheritance,

r value and reference based objects,

r true polymorphism,

r two categories of visibility (public/hidden attributes, public/hidden methods), and

r persistent objects support via required separate module.

3.1. Summary of language changes and extensions

By adopting this OO-extension proposal the syntactic language extensions of ISO Modula-2 are minimal. We could even re-use the keyword RECORD for class types. Only two new keywords CLASS and WITHTYPE and three new pervasive identifiers REDEFINED, NEWOBJ, and DISPOSEOBJ are needed. The latter two are even optional. The reason for not using NEW and DISPOSE for classes is that this allows to later add garbage collection. The existing keywords IS and EXPORT serve additional purposes. Designators allow a type test of the form v(T) as in Oberon-2.

The most important changes are:

r The keyword CLASS before MODULE indicates a combined definition/implementation. Any record type declared in this module would get an associated, unique compiler generated record type descriptor.

r EXPORT marks the exported objects (same as the "*"-mark in Oberon-2).

r The keyword WITHTYPE indicates the with-type statement to test a dynamic type and to apply a typeguard (same as WITH statement in Oberon-2).

r If a method call is qualified by the pervasive identifier REDEFINED, the overriden method is called. The syntax is REDEFINED.receiver.method (same as receiver.method^ in Oberon-2).

r The Oberon-2 syntax of type-bound procedures is adopted for the declarations of methods.

The following chapters on declarations, record, pointer and procedure types, operands, relations, with-type statements and type-bound procedures describe all language changes and extensions that concern the OO-extensions only. These chapters are based on M~ossenb~ock's Oberon-2 Report (revised May-1992) and were slightly modified to fit into this OO-Modula-2 extension proposal. The Oberon-2 terminology (e.g. base class instead of super class, type bound procedure instead of method, record field instead of attribute) is used. The Oberon-2 style of EBNF is used for the syntax specification.

4. Declarations and scope rules

An identifier declared in a module block may be followed by the export mark-keyword EXPORT in its declaration to indicate that it is exported. An identifier x exported by a module M may be used in other modules, if they import M.


Qualident = [ident "."] ident.
IdentDef = ident [EXPORT].
IdentList = IdentDef {"," IdentDef}

5. Constant declarations

Constants can be exported.


ConstantDeclaration = IdentDef "=" ConstExpression.
ConstExpression = Expression.
6. Type declarations

Types can be exported.


TypeDeclaration = IdentDef "=" Type. 
Type = Qualident | ArrayType | RecordType | PointerType | ProcedureType.

6.1. Record types

If a record type is exported, field identifiers that are to be visible outside the declaring module must be marked. They are called public fields; unmarked elements are called private fields.


RecordType = RECORD ["("BaseType")"] FieldList {";" FieldList} END.
BaseType = Qualident. 
FieldList = [IdentList ":" Type ].

Record types declared in class modules are extensible in the same module or in another class module, i.e. a record type can be declared as an extension of another record type. In the example

  T0 = RECORD x: INTEGER END

  T1 = RECORD (T0) y: REAL END

T1 is a (direct) extension of T0 and T0 is the (direct) base type of T1. An extended type T1 consists of the fields of its base type and of the fields which are declared in T1. Identifiers declared in the extension must be different from the identifiers in its base type(s).
6.2. Pointer types

Variables of a pointer type P assume as values pointers to variables of some type T. T is called the pointer base type of P; When declared in a class module, the pointer base type must a record type. Pointer types inherit the extension relation of their pointer base types: if a type T1 is an extension of T, and P1 is of type POINTER TO T1, then P1 is also an extension of P.


PointerType = POINTER TO Type. 

If p is a variable of type P = POINTER TO T, a call of the predeclared procedure NEW(p) allocates a variable of type T in free storage. Storage for pointer types declared in class modules must be allocated with NEWOBJ instead of NEW and deallocated with DISPOSEOBJ instead of DISPOSE (see Chapter 9.2).

A pointer to the allocated variable is assigned to p. p is of type P. The referenced variable p^ (pronounced as p-referenced) is of type T. Any pointer variable may assume the value NIL, which points to no variable at all. All pointer variables are initialized to NIL.

6.3. Procedure types

Procedure variables are not allowed to be of type-bound procedure type.

6.4. Variable declarations

Variables can be exported. Variable declarations introduce variables by defining an identifier and a data type for them.


VariableDeclaration = IdentList ":" Type.

Record and pointer variables have both a static type (the type with which they are declared - simply called their type) and a dynamic type (the type they assume at run time). For pointers and variable parameters of record type the dynamic type may be an extension of their static type. The static type determines which fields of a record are accessible. The dynamic type is used to call type-bound procedures.
7. Expressions

7.1. Operands

New notation for type guard required


Designator = Qualident {"." ident | "[" ExpressionList "]" | "^" |
  "(" Qualident ")"}.
ExpressionList = Expression {"," Expression}.

If r designates a record, then r.f denotes the field f of r or the procedure f bound to the dynamic type of r. If p designates a pointer, p^ denotes the variable which is referenced by p. The designator p^.f may be abbreviated as p.f, i.e. record selectors imply dereferencing. A type guard v(T) asserts that the dynamic type of v is T (or an extension of T), i.e. program execution is aborted, if the dynamic type of v is not T (or an extension of T). Within the designator, v is then regarded as having the static type T. The guard is applicable, if

1. v is a variable parameter of record type or v is a pointer, and if

2. T is an extension of the static type of v

7.2. Relations

The keyword IS which is used for set membership is overloaded and now also serves as a relation for a type test.

The relation yields a BOOLEAN result. v IS T stands for "the dynamic type of v is T (or an extension of T)" and is called a type test. It is applicable if

1. v is a variable parameter of record type or v is a pointer, and if

2. T is an extension of the static type of v

8. Statements

8.1. With-type statement

With-type statements execute a statement sequence depending on the result of a type test and apply a typeguard to every occurrence of the tested variable within this statement sequence.


WithTypeStatement = WITHTYPE Guard DO StatementSequence 
  {"|" Guard DO StatementSequence}
  [ELSE StatementSequence]
  END.
Guard = Qualident ":" Qualident.

If v is a variable parameter of record type or a pointer variable, and if it is of a static type T0, the statement

 WITHTYPE v: T1 DO S1
 | v: T2 DO S2
 ELSE S3
 END

has the following meaning: if the dynamic type of v is T1, then the statement sequence S1 is executed where v is regarded as if it had the static type T1; else if the dynamic type of v is T2, then S2 is executed where v is regarded as if it had the static type T2; else S3 is executed. T1 and T2 must be extensions of T0. If no type test is satisfied and if an else clause is missing the program is aborted.
9. Procedure declarations

9.1. Type-bound procedures

Procedures may be associated with a record type declared in the same class module. The procedures are said to be bound to the record type. The binding is expressed by the type of the receiver in the heading of a procedure declaration. The receiver may be either a variable parameter of record type or a value parameter of type pointer to record. The procedure is bound to the type of the receiver. If it is bound to a pointer type it is also bound to its pointer base type. A procedure bound to a record type is considered local to it.


ProcedureDeclaration = ProcedureHeading ";" ProcedureBody ident.
ProcedureHeading = PROCEDURE [Receiver] IdentDef [FormalParameters].
Receiver = "(" [VAR] ident ":" ident ")".
ProcedureBody = DeclarationSequence [BEGIN StatementSequence] END.
DeclarationSequence =
 {CONST {ConstantDeclaration ";"} | 
  TYPE {TypeDeclaration ";"} | 
  VAR {VariableDeclaration ";"}
 }
 {ProcedureDeclaration ";"}.

If a procedure P is bound to a type T0, it is implicitly also bound to any type T1 which is an extension of T0. However, a procedure P' (with the same name as P) may be explicitly bound to T1 in which case it overrides the binding of P. P' is considered as a redefinition of P for T1. The formal parameters of P and P' must match. If P and T1 are exported P' must be exported too (in class modules).

If v is a designator and P is a type-bound procedure, then v.P denotes that procedure P which is bound to the dynamic type of v (dynamic binding). Note, that this may be a different procedure than the one bound to the static type of v. v is passed to P's receiver according to the standard parameter passing rules.

If r is a receiver parameter declared with type T, REDEFINED.r.P denotes the (redefined) procedure P bound to the base type of T.

Note, that the declaration sequence is more restrictive in class modules when compared to the other module kinds; first constants, types and variables are declared in arbitrary sequence then the declaration of procedures follows.

Not shown in the procedure syntax above is optional exceptional part.

9.2. Predeclared procedures

The following table lists two new predeclared proper procedures. The argument types shall be declared in class modules only.


    Name          Argument types                   Function
______________________________________________________________________________
    NEWOBJ(v)     pointer to record                allocate v^
    DISPOSEOBJ(v) pointer to record                deallocate v^
______________________________________________________________________________

Calls to NEWOBJ and DISPOSEOBJ are substituted by the compiler to calls of the procedures ALLOCATEOBJ and DEALLOCATEOBJ if visible in the callers scope; it is an error if NEWOBJ and DISPOSEOBJ can't be resolved.

The rest of this section is devoted to present the internal data structures of a possible implementation of a type descriptor:


    NEWOBJ(v) is translated into 
      ALLOCATEOBJ(v, SIZE(v^)+"tagSize");
      v^."tagAtOffset0" := "typeTagOf(v)"; (* set the type descriptor *)
      v := SYSTEM.ADDADR(v, "tagSize");

    DISPOSEOBJ(v) is translated into 
      v := SYSTEM.SUBADR(v, "tagSize");
      DEALLOCATEOBJ(v, SIZE(v^)+"tagSize");

"tagSize" is an implementation defined value, usually equal to SIZE(SYSTEM.ADDRESS).

"tagAtOffset0" is a pointer to the type descriptor allocated by the compiler for each record type declared in a class module. The type descriptor itself is of record type TypeDescriptor and "tagAtOffset0" points to the field extensions[0].


TYPE
  TypeName  = ARRAY [0..maxIdentLen-1] OF CHAR;

  TypeDescriptor=RECORD
     methods: ARRAY [-unlimitedNumberOfMethods .. 1] OF PROC;
     extensions: ARRAY [0 .. maxRecordExtensionLevel] OF "RecordKey" 
     moduleName, typeName: TypeName;
  END;

"RecordKey" is a (perfect) hash code of the type desciptor's moduleName together with the symbol file's alphabetically sorted object reference count value.
10. Class Modules

A class module is a collection of declarations of constants, types, variables, and procedures, together with a sequence of statements (initialization, module body). A class module constitutes a text that is compilable as a unit.


ClassModule = CLASS MODULE ident ";" [ImportList] DeclarationSequence 
  [BEGIN StatementSequence] END ident ".".

The import list specifies the names of the imported modules. If a module A is imported by a module M and A exports an identifier x, then x is referred to as A.x within M. Identifiers that are to be exported (i.e. that are to be visible in client modules) must be marked by an export mark in their declaration (see Chapter 4). The rules for the execution of the statement sequence following the symbol BEGIN are the same as with regular implementation modules. It follows that cyclic import of class modules is illegal.

Not shown in the module syntax above are optional finalization body and exceptional part of class modules.

The syntactic form and the usage of the keywords CLASS and EXPORT, the pervasive identifier REDEFINED, the redefinition of methods, with-type statements, type test, type guard and object allocation/deallocation (see Chapter 9.2 and 11.1) are illustrated with the following simple example:


___________________________________________________________________

CLASS MODULE name;
IMPORT ObjStorage;
CONST
    ALLOCATEOBJ =ObjStorage.ALLOCATEOBJ;
  DEALLOCATEOBJ =ObjStorage.DEALLOCATEOBJ;

CONST
  x EXPORT=0;             (* x is exported; x*=0; in Oberon-2 *)
  y=0;                    (* y is not exported *)

TYPE
  p EXPORT=POINTER TO T; (* p is exported *)
  T EXPORT=RECORD        (* type T is exported *)
      a,
      b:        CHAR;    (* fields a,b are invisible outside *)
      c EXPORT,
      d EXPORT: REAL     (* fields c,d are accessible outside *)
  END;

  p1 EXPORT=POINTER TO T1;
  T1 EXPORT= RECORD (T)
     e: BOOLEAN;
  END;

VAR 
  vp: p;
  vp1: p1;      (* vp,vp1 are not exported *)
  v EXPORT,
  w EXPORT: T;  (* v,w are exported *)
  z: REAL;      (* z is not exported *)       

PROCEDURE (receiver: p) m1 EXPORT (param: T); (* method m1 is exported *)
(*PROCEDURE (receiver: p) m1*     (param: T); (* in Oberon-2 *)*)
BEGIN
  IF receiver IS p1 THEN
    receiver(p1).e := FALSE
    (* explicit type test; syntactically identical to Oberon-2 *)
  ELSE HALT
  END;

  WITHTYPE receiver: p1 DO
(*WITH     receiver: p1 DO (* in Oberon-2 *)*)
    receiver.e:=FALSE;
  END; 
END m1;

PROCEDURE (receiver: p1) m1 EXPORT (param: T);(*redefined method m1 is exported
 *)
BEGIN
  REDEFINED.receiver.m1(param); (* calls anchestor of method m1 *)
(*receiver.m1^(param);          (* in Oberon-2 *)*)
END m1;

PROCEDURE (r: p) m2 (param: T): T; (* method m2 is hidden, i.e. not exported *)

BEGIN
END m2;

PROCEDURE proc1 EXPORT (param: T); (* procedure proc1 is exported *)
BEGIN
END proc1;

PROCEDURE m2 (p: T);(*procedure m2 is not exported; the name m2 could be re-use
d*)
BEGIN
END m2;

BEGIN
  NEWOBJ(vp1);

  vp := vp1; (* the assignment sets the dynamic type of vp to p1 *)
  vp.m1(v);  (* calls overriden method m1 *)

  DISPOSEOBJ(vp);
END name.
___________________________________________________________________

Here is the definition of the example class module name, which is generated by the compiler:


___________________________________________________________________

CLASS DEFINITION MODULE name;
CONST
  x=0;

TYPE
  p=POINTER TO T;
  T=RECORD
    c,d: REAL
  END;

  p1=POINTER TO T1;
  T1 = RECORD (T)
  END;

VAR
  v, w: T;

PROCEDURE (receiver: p) m1 (param: T);
PROCEDURE (r: p1) m1 (param: T);
PROCEDURE proc1 (param: T);

END name.
___________________________________________________________________
11. Required separate modules

11.1. Module ObjStorage

Module ObjStorage is identical to the required ISO Modula-2 module Storage except for that procedures and types are prefixed by "Obj". The compiler is responsible for requesting additional storage for the type descriptor pointer (tag) and for assigning the correct value to the tag.


___________________________________________________________________

DEFINITION MODULE ObjStorage;
  (* Facilities for dynamically allocating and deallocating storage *)

IMPORT SYSTEM;

PROCEDURE ALLOCATEOBJ (VAR addr: SYSTEM.ADDRESS; amount: CARDINAL);
(* Allocates storage for a variable of size amount and assigns the address of 
   this variable to addr. If there is insufficient unallocated storage to do 
   this, the value NIL is assigned to addr.
 *)

PROCEDURE DEALLOCATEOBJ (VAR addr: SYSTEM.ADDRESS; amount: CARDINAL);
(* Deallocates amount locations allocated by ALLOCATE for the storage of the
   variable addressed by addr and assigns the value NIL to addr.
 *)

TYPE
  ObjStorageExceptions = (
    nilDeallocation,             (* first argument to DEALLOCATEOBJ is NIL *)
    pointerToUnallocatedStorage, (* storage to deallocate not allocated by
                                    ALLOCATEOBJ *)
    wrongStorageToUnallocate   (* amount to deallocate is not amount allocated 
*)
  );

PROCEDURE IsObjStorageException (): BOOLEAN;
(* Returns TRUE if the current coroutine is in the exceptional execution state
   because of the raising of an exception from ObjStorageExceptions;
   otherwise returns FALSE.
*)

PROCEDURE ObjStorageException (): ObjStorageExceptions;
(* If the current coroutine is in the exceptional execution state because of th
e
   raising of an exception from ObjStorageExceptions, returns the corresponding

   enumeration value, and otherwise raises an exception.
 *)

END ObjStorage.

___________________________________________________________________

11.2. Module Objects_Types

The required standard module Objects_Types provides basic functionality to access the dynamic type of objects, to create and to dispose objects of dynamic type and supports persistent objects.



___________________________________________________________________

CLASS DEFINITION MODULE Objects_Types;

CONST maxIdentLen = 32;

TYPE
  Object = POINTER TO ObjectDesc;
  ObjectDesc = RECORD END;

  Type = POINTER TO TypeDesc;

TYPE
  Objects_TypesExceptions = (
    noTypeDescriptor, 
    wrongModuleTypeName,
    nilDisposal (*, ...? *)
  );

PROCEDURE TypeOf (o: Object): Type;
(* returns the unique dynamic type of o
 *)

PROCEDURE TypeName (typ: Type; VAR module, name: ARRAY OF CHAR);
(* module and name are filled from LENGTH() to LEN()-1 with CHR(0)
 *)

PROCEDURE This (module, name: ARRAY OF CHAR): Type;
(* returns the unique type associated with module name and type name
 *)

PROCEDURE NewObj (VAR o: Object; t: Type);
(* creates a new object of type t. 
   Allocation is performed by calling ObjStorage.ALLOCATEOBJ
 *)

PROCEDURE DisposeObj  (VAR o: Object);
(* disposes an object o pointing to an extension of Objects_Types.ObjectDesc
  depending on the dynamic type of o (including the storage occupied by the
  type tag). Deallocation is performed by calling ObjStorage.DEALLOCATEOBJ
 *)

PROCEDURE SizeOf (o: Object) : CARDINAL;
(* returns the number of bytes allocated to the dynamic type of the object o,
   not counting the size of the type tag (see 9.2).
 *)

(* Exception handling: *)

PROCEDURE IsObjects_TypesException (): BOOLEAN;
  (* Returns TRUE if the current coroutine is in the exceptional execution state
     because of the raising of an exception from Objects_TypesExceptions;
     otherwise returns FALSE.
  *)

PROCEDURE Objects_TypesException (): Objects_TypesExceptions;
(* If the current coroutine is in the exceptional execution state because of the
   raising of an exception from Objects_TypesExceptions, returns the 
   corresponding enumeration value, and otherwise raises an exception.
 *)

(* The following procedure is exported,
    but only callable by the compiler during class module initialization:
 *)
PROCEDURE StoreModObjects (typeDescBase, objects: SYSTEM.ADDRESS);
(* StoreModObjects is automatically and indirectly called in the module body of

   any class module to register all record types declared within a module.
 *)

END Objects_Types.

___________________________________________________________________

Note, uniqueness of persistent types is required to be able to correctly restore persistent data. The name pair of module and type is unique within the same project for all non-anonymous record types. Anonymous types can't be persistent; this is checked in the procedures TypeName and This and any attempt to store an object based on an anonymous record type, would result in a noTypeDescriptor exception. Procedure This raises wrongModuleTypeName exception, if the parameters module or name are invalid.

A problem arises, when the data structure of a type is changed but the type name is not. The data base administator is responsible to guarantee the data integrity (e.g. by storing a version number in the header of a permanent database).

I don't see any solution to achieve full security even when facilities are provided to retrieve the full data type information, that describes the structure of a persistent type at run-time. No security is gained when comparing the type structure description contained in permanent memory to that of the actual type derived from type name. For example, consider a semantic change of one record field:


TYPE
  T=RECORD                           T=RECORD
    cash: INTEGER (* in US$ *)         cash: INTEGER (* in millions of US$ *)
  END                                END

Neither the data type structure nor the module name has changed but the data is going to be interpreted differently. Who makes sure that a data conversion procedure is automatically called at restoration of any persistent element of the old record type T?

Also it would not help to generate a hash code of module- and type name or type structure combined with the symbol file key. When modifying other interface related objects of the same class module, a new symbol file with a new key could be generated. This would invalidate all other persistent objects whose type is declared in the same class module.

12. Symbol file and definition module generation

The symbol file handling is similar to that of Oberon-2. For those not familiar with the Oberon strategy: The compiler generates the symbol file each time when the class module is compiled. If there are no compilation errors, all objects to be exported (constants, types, vars, [type-bound] [hidden] procedures) are sorted alphabetically. Anonymous types are sorted in their textual order. Then a new symbol file is written. If the old symbol file differs only in the module key, then the new symbol file is deleted otherwise the new symbol file replaces the old one and a new definition module is generated automatically. The latter could also be deferred to a browser tool, but the compiler could keep the original textual order of declarations which would be lost when generated from the symbol file.

Since the type-bound procedures contain an explicit receiver specification, they could either be grouped together and listed within their associated class definitions (as does the Oberon browser) or outside the class definition as in the class (implementation) module. The latter form was choosen, because it looks more orthogonal in case of non-exported pointer base types.

One example for this situation is type table in the class module OAVL (see Chapter 13). Type table is not a pointer to a record type without visible attributes, but rather a pointer to a hidden record type. The type tableheader itself is not exported hence it doesn't have a record declaration. If it would have a record declaration, clients would be able to extend that class which is not desired in the module OAVL.

Hence the compiler lists table as a hidden type in the generated class definition module. It follows hidden types are implemented as pointers to records in class modules.

13. Example: a generic table handler

This example is not artificial; it shows a run-time type-safe generic table handler (AVL-tree) with persistent objects support and it was cut and pasted off a real world application example. Historically, it was converted from a client's Modula-2 application program to Oberon-2 to illustrate the power of OOP.

Module Use_OAVL inserts data elements into a tree instantiated with the type of a private extension of the base type by calling the type-bound procedure insert. Then it displays the data in the sequence defined by the overriden methods order and equal. Display is accomplished by calling the type-bound procedure visit parameterized with the procedure PrintOutRec to be called for each data element. Then it saves the data elements to permanent storage (datafile) by calling the type bound procedure save and retrieves the data by calling the procedures OAVL.load which generates a new table instance. Its type is determined at run-time by the objects read from the datafile.

If the table's dynamic type happens to be different to Use_OAVL's Object-extension, the new type will automatically have it's own set of comparison methods "attached" to it. Although the test for mytype after OAVL.load assures that the persistent data are of the expected type, the else-part of the type guard in procedure PrintOutRec shows how to check for the dynamic type of Object.

The OAVL-example not only shows the OO-language extensions, it also shows how to use the separate module called Objects_Types, which provides run-time type information and basic features needed to generate and clone objects derived from the base class ObjectsDesc. I/O is accomplished with the ISO Modula-2 library modules SeqFile and RawIO.

For this paper, I've simplified the data type in client Use_OAVL and converted the application from Oberon-2 to what could eventually turn out to be OOE-ISO Modula-2. (In the meantime, you are encouraged to call this language Obula. :-)

First the definition module of the table handler OAVL how it would be generated by the compiler is listed. The exported objects are contained in their textual order of appearance in the class module OAVL.



___________________________________________________________________

CLASS DEFINITION MODULE OAVL;

IMPORT
  Objects_Types;

TYPE
  Type = Objects_Types.Type;
  Object = POINTER TO ObjectDesc;
  ObjectDesc = RECORD (Objects_Types.ObjectDesc) END;
  visittype = PROCEDURE (VAR Object);
           
TYPE
  table; (* hidden *)

PROCEDURE TypeOf (o: Object): Type;
PROCEDURE (info: Object) order (item: Object): BOOLEAN;
PROCEDURE (info: Object) equal (item: Object): BOOLEAN;
PROCEDURE define (VAR t: table; type: Type);
PROCEDURE (t: table) type (): Type;
PROCEDURE (t: table) insert (w : Object);
PROCEDURE (t: table) delete (item : Object);
PROCEDURE (t: table) empty (): BOOLEAN;
PROCEDURE (t: table) visit (visitproc: visittype);
PROCEDURE (t: table) makeempty;
PROCEDURE (t: table) save (fn: ARRAY OF CHAR);
PROCEDURE load (VAR t: table; fn: ARRAY OF CHAR);

END OAVL.

___________________________________________________________________
Use_OAVL is an example for a client module of OAVL:

___________________________________________________________________

CLASS MODULE USE_OAVL;

IMPORT OAVL, SRealIO;
FROM ObjStorage ALLOCATEOBJ, DEALLOCATEOBJ;

  CONST max=1000;
     datafile="x.dat";

  TYPE
    Object = POINTER TO ObjectDesc;

    ObjectDesc = RECORD (OAVL.ObjectDesc)
      pu: REAL;
    END;

  VAR myobj, out: Object;
      mytype: OAVL.Type;
      tab: OAVL.table;
      i: INTEGER;

  PROCEDURE PrintOutRec(VAR obj: OAVL.Object);
  BEGIN
    WITHTYPE obj : Object DO
      SRealIO.WriteReal(obj^.pu, 13);
    ELSE HALT
    END;
  END PrintOutRec;

  PROCEDURE (obj1 : Object) equal EXPORT (obj2 : OAVL.Object) : BOOLEAN;
  BEGIN
    WITHTYPE obj2 : Object DO
      RETURN obj1^.pu = obj2^.pu
    END;
  END equal;

  PROCEDURE (obj1 : Object) order EXPORT (obj2 : OAVL.Object) : BOOLEAN;
  BEGIN
    WITHTYPE obj2 : Object DO
      RETURN obj1^.pu < obj2^.pu;
    END;
  END order;

BEGIN
  NEWOBJ(myobj);
  mytype := OAVL.TypeOf(myobj);
  DISPOSEOBJ(myobj)

  OAVL.define(tab, mytype);

  FOR i:=max TO 0 BY -1 DO
    NEWOBJ(out);
    out^.pu:=LFLOAT(i);
    tab.insert(out);
  END;
  tab.visit(PrintOutRec); 

  tab.save(datafile);
 
  OAVL.load(tab,datafile);
  IF tab.type() = mytype THEN
    tab.visit(PrintOutRec);
  ELSE HALT
  END;

  tab.makeempty;
END USE_OAVL.

___________________________________________________________________
Implementation of class module OAVL is listed below. To save space, the error handling was simply replaced by calls to HALT and some parts of the non-object oriented, tree-handling algorithm were cut out.

The algorithm for type name compressions by indexing closely follows that of M~ossenb~ock, first published in the chapter on persistent objects in his book Object Oriented Programming in Oberon-2 (For a complete example, see also The ModulaTor, Vol 3, Nr. 1.)

In contrast to M~ossenb~ock's implementation, the procedures WriteObj and ReadObj are generic in module OAVL, in that they don't rely (and don't need) to call methods which must be written by the clients of OAVL. This can be done because the tree nodes contains an info-pointer which allows to access the client's data extension. The size of the dynamic data type is determined by calling the procedure Object_Types.SizeOf.

M~ossenb~ock's mechanism requires that the client overrides the save and load methods each extension of OAVL.ObjectDesc. In general, this can't be avoided when the clients extension contain pointer type fields or other non-persistent fields.

In order to guarantee that no elements of different extensions of the base class are inserted or deleted in the same table, OAVL.define has a second parameter of type Objects_Types.Type which is stored in the table's header. OAVL is designed to handle only one element type per table instance, otherwise elements would not be comparable. The methods order and equal are used for balancing the AVL-tree. The table's type shall match the dynamic type of each element to be inserted or deleted, otherwise an exception is generated (by calling HALT).



___________________________________________________________________

CLASS MODULE OAVL;
(* Obula version of Oberon-2's version of Modula-2's generic table handler modu
le.
   - Full type safety is guaranteed for all extensions of
     the data type OAVL.Object
   - Full generic with persistent objects support for data stored/loaded
     to/from file.
*)
IMPORT Objects_Types, RawIO, IOChan, Strings, ObjStorage, SYSTEM;

CONST
    ALLOCATEOBJ =ObjStorage.ALLOCATEOBJ;
  DEALLOCATEOBJ =ObjStorage.DEALLOCATEOBJ;

TYPE

  Type EXPORT= Objects_Types.Type;

  Object EXPORT= POINTER TO ObjectDesc;
  ObjectDesc EXPORT= RECORD (Objects_Types.ObjectDesc) END;
           
  nodepointer = POINTER TO tablenode;

  tablenode = RECORD
    left: nodepointer;
    right: nodepointer;
    bal : INTEGER;
    info : Object;
  END;

  table EXPORT= POINTER TO tableheader;
                      
  tableheader = RECORD
    root: nodepointer;
    objtype: Type;
  END;

  visittype EXPORT= PROCEDURE(VAR Object);

(* persistent object support *)
CONST
  maxIdentLen  = Objects_Types.maxIdentLen;
  maxNames = 256; (* used for module-, type-name compression *)
  noName = "";

TYPE
  TypeName  = ARRAY [0..maxIdentLen-1] OF CHAR;
  ModuleTypeName  = RECORD
    module, type: TypeName;
  END;

  Stream  = RECORD
    file : IOChan.ChanId;
    tab: ARRAY [0..maxNames-1] OF TypeName; (* tab[0]="" *)
    end: INTEGER (* tab[0..end-1] are filled *)
  END;

PROCEDURE TypeOf EXPORT (o: Object): Type;
(* re-exported to avoid Object_Types import in the client's module *)
BEGIN
  RETURN Objects_Types.TypeOf(o);
END TypeOf;   

PROCEDURE ReadNBytes (VAR f: IOChan.ChanId; bufPtr: SYSTEM.ADDRESS;
  reqBytes: CARDINAL; VAR ok: BOOLEAN );
VAR i, read: CARDINAL;
BEGIN
  read := 0;     
  FOR i := 1 TO reqBytes DO
    RawIO.Read(f, bufPtr^);
    bufPtr := SYSTEM.ADDADR(bufPtr, 1);
    INC(read);
  END;
  ok := read = reqBytes;
END ReadNBytes;
                
PROCEDURE WriteNBytes (VAR f: IOChan.ChanId; bufPtr: SYSTEM.ADDRESS;
  reqBytes: CARDINAL; VAR ok: BOOLEAN);
VAR i, written: CARDINAL;
BEGIN
  written := 0;     
  FOR i := 1 TO reqBytes DO
    RawIO.Write(f, bufPtr^);
    bufPtr := SYSTEM.ADDADR(bufPtr,1);
    INC(written);
  END;
  ok := written = reqBytes; END WriteNBytes;

PROCEDURE (VAR r: Stream) WriteString  (s: TypeName);
(* Type name index compression for save; see The ModulaTor [3,1] *)
VAR i: INTEGER;
BEGIN
  i:=0;
  LOOP (* search s in r.tab *)
    IF i=r.end THEN (* first occurence of s *)
      RawIO.Write(r.file,i);
      RawIO.Write(r.file,s);
      r.tab[r.end]:=s; INC(r.end);
      EXIT
    ELSIF Strings.Equal(s, r.tab[i]) THEN        
      RawIO.Write(r.file,i);
      EXIT;
    ELSE INC(i);
    END;
  END;
END WriteString;

PROCEDURE (VAR r: Stream) ReadString  (VAR s: TypeName);
(* Type name index decompression; see The ModulaTor [3,1] *)
VAR i: INTEGER;
BEGIN
  RawIO.Read(r.file, i);
  IF i = r.end THEN (* full text follows *)
    RawIO.Read(r.file,s);
    r.tab[r.end]:=s; INC(r.end);
  ELSE
    s :=r.tab[i];
  END;  
END ReadString;

PROCEDURE WriteObj  (VAR r: Stream; x: Object; VAR ok : BOOLEAN);
(* see The ModulaTor [3,1], modified to WriteNBytes instead of calling
   a store method *)
VAR module,name: TypeName;
BEGIN
  IF x=NIL THEN r.WriteString(noName); ok:=TRUE;
  ELSE 
    Objects_Types.TypeName(Objects_Types.TypeOf(x),module,name); 
    r.WriteString(module);
    r.WriteString(name);
    WriteNBytes(r.file, x, Objects_Types.SizeOf(x), ok);
  END;
END WriteObj;

PROCEDURE ReadObj  (VAR r: Stream; VAR x: Object; VAR ok : BOOLEAN);
(* see The ModulaTor [3,1], modified to ReadNBytes instead of calling
   a load method *)
VAR module,name: TypeName; y: Objects_Types.Object;
BEGIN
  r.ReadString(module);
  IF module[0]=CHR(0) THEN x:=NIL; ok:=TRUE;
  ELSE
    r.ReadString(name);
    Objects_Types.NewObj(y,Objects_Types.This(module,name));
    (* temporary variable y of base type and type test necessary. *)
    x:=y(Object);
    ReadNBytes(r.file,x,Objects_Types.SizeOf(x), ok);
  END;
END ReadObj;

PROCEDURE (info : Object) order EXPORT (item : Object): BOOLEAN;
BEGIN HALT
END order;

PROCEDURE (info : Object) equal EXPORT (item : Object): BOOLEAN;
BEGIN HALT (* it can't happen here *)
END equal;
                            
PROCEDURE (item : table) init (type: Type);
BEGIN
  item.root := NIL;
  item.objtype := type;
END init;

PROCEDURE define EXPORT (VAR t : table; type: Type);
BEGIN
  NEWOBJ(t);
  t.init(type);
END define;

PROCEDURE (t: table) type EXPORT (): Type;
BEGIN
  RETURN t.objtype;
END define;

PROCEDURE (t : table) empty EXPORT (): BOOLEAN;
BEGIN
  RETURN t.root = NIL
END empty;

PROCEDURE (tree : table) insert EXPORT (w : Object);
VAR p1, p2 : nodepointer;
    h : BOOLEAN;

  PROCEDURE AVLInsert (w : Object; VAR p : nodepointer; VAR h: BOOLEAN);
  BEGIN
    IF p = NIL THEN
      h := TRUE;
      p := getnode(w);
    ELSIF w.order(p^.info) THEN
      AVLInsert( w,  p^.left,  h );
      ...
    ELSIF ~ w.equal(p^.info) THEN
      AVLInsert( w,  p^.right,  h );
      ...
    ELSE
      h  := FALSE;
    END
  END AVLInsert;

BEGIN
  IF tree # NIL THEN
    IF Objects_Types.TypeOf(item) # tree^.objtype THEN HALT; END;
    AVLInsert(w,tree^.root,h);
  END;
END insert;
  
PROCEDURE (tree : table) delete EXPORT (item : Object);
VAR h : BOOLEAN;

PROCEDURE AVLDelete (x : Object; p : nodepointer; VAR h : BOOLEAN);
VAR q,mark : nodepointer;
  
  PROCEDURE balance1 ( p : nodepointer; VAR h : BOOLEAN);
  ...
  END balance1;

  PROCEDURE balance2 (p : nodepointer; VAR h: BOOLEAN);
  ...
  END balance2;

  PROCEDURE del (r : nodepointer; VAR h: BOOLEAN);
  VAR item1 : Objects_Types.Object;
  BEGIN
    IF ~ (r^.right = NIL ) THEN
      del( r^.right, h );
      IF h THEN balance2( r, h ) END;
    ELSE
      item1 := p^.info;
      Objects_Types.DisposeObj(item1);
      p^.info := r^.info;
      mark := r;
      r := r^.left;
      DISPOSEOBJ(r);
      h := TRUE;
    END;
  END del;

  BEGIN
    IF p = NIL THEN HALT;
    ELSIF x.order(p^.info) THEN
      AVLDelete( x, p^.left, h );
      IF h THEN balance1( p, h ) END;
    ELSIF ~ x.equal(p^.info)) THEN
      AVLDelete( x, p^.right, h );
      IF h THEN balance2( p, h ) END;
    ELSIF p^.right = NIL THEN
      q := p; p := q^.left; h := TRUE;
      DISPOSEOBJ(q);
    ELSIF p^.left = NIL THEN
      q := p; p := q^.right; h := TRUE;
      DISPOSEOBJ(q);
    ELSE
      del( p^.left,  h );
      IF h THEN  balance1( p,  h ) END;
    END
  END AVLDelete;
    
BEGIN
  IF tree # NIL THEN
    IF Objects_Types.TypeOf(item) # tree^.objtype THEN HALT; END;
    AVLDelete( item, tree^.root, h )
  END
END delete;

PROCEDURE ( t: table) visit EXPORT (visitproc: visittype);

   PROCEDURE visitation ( p : nodepointer);
   BEGIN
     IF p # NIL THEN
       visitation( p^.left );
       visitproc( p^.info );
       visitation( p^.right );
     END;
   END visitation;
   
 BEGIN
   IF t # NIL THEN
     IF t.empty() THEN HALT;
     ELSE visitation( t^.root );
     END
   END
 END visit;

 PROCEDURE ( t : table) makeempty EXPORT;

   PROCEDURE posttrav ( VAR n: nodepointer);
   VAR item1 : Objects_Types.Object;
   BEGIN
     IF n # NIL THEN
       posttrav( n^.left );
       posttrav( n^.right );
       item1 := n^.info;
       n^.info := NIL;
       Objects_Types.DisposeObj(item1);
       DISPOSEOBJ(n);
     END
   END posttrav;
   
 BEGIN
   IF t # NIL THEN
     posttrav( t.root );
     t.root := NIL;
   END
 END makeempty;

 PROCEDURE InitStream(VAR s: Stream);
 BEGIN
   s.tab[0]:=""; 
   s.end:=1;
 END InitStream;

 PROCEDURE (t : table) save EXPORT (fn: ARRAY OF CHAR);
 (* Generic save routine. Stores type information for persistent objects *)
 VAR alright : BOOLEAN;
   s : Stream;
   ores: SeqFile.OpenResults;
                                      
   PROCEDURE visitsave(p: nodepointer);
   VAR ok : BOOLEAN;
   BEGIN
     IF p # NIL THEN
       visitsave(p^.left);
       WriteObj(s,p.info,ok);
       IF ~ ok THEN HALT; END;
       visitsave(p^.right);
     END;
   END visitsave;                          

 BEGIN
   InitStream(s);
   IF t # NIL THEN
     IF ~ t.empty() THEN                
       SeqFile.OpenWrite(s.file, fn, SeqFile.binary, ores);
       IF ores=SeqFile.opened THEN
         visitsave( t^.root );
         WriteObj(s,NIL,alright); (* eof *)
         SeqFile.Close( s.file );
       ELSE HALT;
       END;
     END;
   END;                 
 END save;

 PROCEDURE load EXPORT (VAR t: table; fn: ARRAY OF CHAR);
 (* Generic load routine. Loads type information for persistent
    objects and generates an object pointer of the 
    associated dynamic type
 *)
 VAR ok   : BOOLEAN;
     new  : Object;
     s : Stream;
     ores: SeqFile.OpenResults;
 BEGIN
   InitStream(s);
   IF t # NIL THEN
     t.makeempty;
     SeqFile.OpenRead(s.file, fn, SeqFile.binary, ores);
     IF ores=SeqFile.opened THEN
        LOOP
          ReadObj(s, new, ok);
          IF ~ ok THEN HALT
          ELSIF new = NIL THEN EXIT
          ELSE t.insert(new)
          END;
        END;
       SeqFile.Close( s.file );
     ELSE HALT;
     END
   END;
 END load;

END OAVL.

___________________________________________________________________

14. Summary

The simplicity gained by the restriction to single inheritance combined with true polymorphism are the strengths of Oberon-2. As shown by J. Templ in A Systematic Approach to Multiple Inheritance, SIGPLAN Notices, Apr-1993, multiple inheritance does not increase the expressive power and is a purely syntactical advantage.

This article showed that simple, yet powerful and well understood OO-language features of Oberon-2 can be integrated into ISO Modula-2 with minimal syntactic extension by the introduction of a new class module type.

By providing only six basic procedures to handle run-time type information in a required separate module, persistent objects can be handled efficiently too.

Integrating definition and implementation into a single class module significantly improves module maintainability.

A non-trivial run-time type-save, generic tree-handler application was implemented using the proposed OOE of ISO Modula-2 and served to illustrate most of the new features.

Acknowledgement: I like to thank Hartmut Goebel for his suggestions for this proposal. I also thank Petra Fabian for her help in converting the design of the original module OAVL from Modula-2 to Oberon-2 and A. Schuhmacher for her corrections. Thanks also to our VAX/VMS Modula-2 compiler customer who provided the original Modula-2 implementation of the AVL-tree handler together with a complete and large set of real-world test data.

Remarks for the 2nd version of this proposal:

After submitting the first draft of this proposal to the sc22.wg13 committee (the number OO9 was assigned to this article), a number of comments were made:

1. At the DIN-AK (German Modula-2 working group of sc22.wg13) meeting in Renningen, 08-May-1993, Elmar Henne pointed out that although this proposal is the most complete but with the least chance to get accepted by the committee, mostly because of the implicit class definition module. He compared it with the other proposals of Albert Wiedemann (OO7), Richard Thomas (OO8), and Pat Terry's yet pending OOE-proposal which crafted after the FST (Fitted Software Tools) Modula-2 compiler's OO-extension.

At the Renningen meeting, attended by more than 12 persons, I suggested to vote pro or contra implicit class definition modules, just to get a feeling whether this proposal would be acceptable. Elmar Henne and Albert Wiedemann did not allow the voting.

2. At the same meeting, Christof Brass said that he wants to keep the explicit class definition modules for now but suggested to remove explicit definition modules altogether in the next major revision of the language later.

3. Peter Grill was not able to come to the meeting, but he contributed a paper (written in German) in which he made clear that he also prefers my proposal and likes the automatic generation of class definition modules.

4. Hartmut Goebel (the co-author of the VAX/VMS Oberon-2 compiler back-end) was not able to come to the meeting, but after reading the other OOE-proposals he still prefers this proposal.

5. Dieter Jaeger (the author of the MCS Modula-2 Cross Compiler System; since 1987, he wrote a portable Modula-2 front-end and three different back-ends) was not able to come to the meeting, but after reading my proposal, he called me and said that he fully supports it.

6. Keith Hopper sent me an Email which published in the forum section of The ModulaTor, Jun-93.

These reactions (2, 3, 4, and 5 above) encouraged me to make some minor corrections and to complete this proposal with the specification of the additional compatibility rules in Appendix A.

Appendix A: Definition of terms

The definition of the terms same type and equal type are auxiliary definitions used to explain the compatibility rules below.

Same types

Two variables a and b with types Ta and Tb are of the same type if

1. Ta and Tb are both denoted by the same type identifier, or

2. Ta is declared to equal Tb in a type declaration of the form Ta =Tb, or

3. a and b appear in the same identifier list in a variable, record field, or formal parameter declaration and are not open arrays.

Equal types

Two types Ta and Tb are equal if

1. Ta and Tb are the same type, or

2. Ta and Tb are open array types with equal element types.

3. Ta and Tb are procedure types whose formal parameter lists match.

Type extension (base type)

Given a type declaration Tb = RECORD (Ta) ... END, Tb is a direct extension of Ta, and Ta is a direct base type of Tb. A type Tb is an extension of a type Ta (Ta is a base type of Tb) if

1. Ta and Tb are the same types, or

2. Tb is a direct extension of an extension of Ta

If Pa = POINTER TO Ta and Pb= POINTER TO Tb, Pb is an extension of Pa (Pa is a base type of Pb) if Tb is an extension of Ta.

Note, that variant records are not allowed in class modules.

Assignment compatible

In addition to the ISO Modula-2 assignment compatibility rules, an expression e of type Te is assignment compatible with a variable v of type Tv if one of the following conditions hold:

1. Te and Tv are record types and Te is an extension of Tv and the dynamic type of v is Tv;

2. Te and Tv are pointer types and Te is an extension of Tv;

Expression compatible

In addition to the ISO Modula-2 assignment compatibility rules, for a given operator, the types of its operands are expression compatible if they conform to the following table (which shows also the result type of the expression). T1 shall be an extension of T0.


  operator      first operand   second operand   result type
  ______________________________________________________________________________
  IS              Type T0         Type T1          BOOLEAN
  ___________________________________________________________________

ModulaTor Forum



Date: Tue, 16 Feb 93 13:31:40 +1300
From: Keith Hopper <kh@athens.cs.waikato.ac.nz>
Subject: Portable bit-summing!
To: 100023.2527@compuserve.com

Guenter,

I was pleased to read Vol 2, Nos 10 & 11 of ModulaTor on return from holiday yesterday. I then dived into my 'bits box' of software scraps this morning and came up with a fully portable version of Bit-summing (so far as I can tell). It works on any size of processor and under any compiler (that I know of that 'conforms' to the standard)!


     .
     .
     IMPORT
          SYSTEM ;

     .
     .
     CONST
          One_Left = -1 ;

     .
     .
     TYPE
          Bits = CARDINAL[0 .. (SIZE(CARDINAL) * SYSTEM.BITSPERLOC - 1)] ;

PROCEDURE BitSum_4(
                    Num : CARDINAL
                    ) : Bits ;

     TYPE
          Card_Set = PACKEDSET OF Bits ;

     VAR
          Ans
               : Bits ;

     BEGIN
          Ans := 0 ;

          LOOP
               IF Num = 0 THEN
                    RETURN Ans
               ELSE
                    IF ODD(Num) THEN
                         INC(Ans)
                    END ;

                    Num := SYSTEM.CAST(CARDINAL,
                              SYSTEM.SHIFT(SYSTEM.CAST(Card_Set,Num),One_Left))

               END
          END
     END BitSum_4 ;

     .
     .

Just thought you might be interested in another view (from 'upside-down' in NZ of course!)

Regards,

Keith


IMPRESSUM: The ModulaTor is an unrefereed journal. Technical papers are to be taken as working papers and personal rather than organizational statements. Items are printed at the discretion of the Editor based upon his judgement on the interest and relevancy to the readership. Letters, announcements, and other items of professional interest are selected on the same basis. Office of publication: The Editor of The ModulaTor is Guenter Dotzel; he can be reached by tel/fax: [removed due to abuse] or by mailto:[email deleted due to spam]


Home Site_index Contact Legal Buy_products OpenVMS_compiler Alpha_Oberon_System DOS_compiler ModulaTor Bibliography Oberon[-2]_links Modula-2_links

Amazon.com [3KB] [Any browser]


Webdesign by www.otolo.com/webworx, 14-nov-1998