Appendix D: The Oberon Environment

Oberon-2 programs usually run in an environment that provides command activation, garbage collection, dynamic loadingof modules, and certain run time data structures. Although not part of the language, this environment contributes to the power of Oberon-2 and is to some degree implied by the language definition. Appendix D describes the essential features of a typical Oberon environment and provides implementation hints. More details can be found in [1], [2], and [3].

D1. Commands

A command is any parameterless procedure P that is exported from a module M. It is denoted by M.P and can be activated under this name from the shell of the operating system. In Oberon, a user invokes commands instead of programs or modules. This gives him a finer grain of control and allows modules with multiple entry points. When a command M.P is invoked, the module M is dynamically loaded unless it is already in memory (see D2) and the procedure P is executed. When P terminates, M remains loaded. All global variables and data structures that can be reached from global pointer variables in M retain their values. When P (or another command of M) is invoked again, it may continue to use these values.

The following module demonstrates the use of commands. It implements an abstract data structure Counter that encapsulates a counter variable and provides commands to increment and print its value.

MODULE Counter;
  IMPORT Texts, Oberon;
  VAR
    counter: LONGINT;
    w: Texts.Writer;

  PROCEDURE Add*;   (* takes a numeric argument from the command line *)
    VAR s: Texts.Scanner;
  BEGIN 
    Texts.OpenScanner(s, Oberon.Par.text, Oberon.Par.pos);
    Texts.Scan(s);
    IF s.class = Texts.Int THEN INC(counter, s.i) END
  END Add;

  PROCEDURE Write*;
  BEGIN
    Texts.WriteInt(w, counter, 5); Texts.WriteLn(w);
    Texts.Append(Oberon.Log, w.buf)
  END Write;

BEGIN counter := 0; Texts.OpenWriter(w)
END Counter.
The user may execute the following two commands:
Counter.Add n
adds the value n to the variable counter
Counter.Write
writes the current value of counter to the screen
Since commands are parameterless they have to get their arguments from the operating system. In general, commands are free to take arguments from everywhere (e.g. from the text following the command, from the most recent selection, or from a marked viewer). The command Add uses a scanner (a data type provided by the Oberon system) to read the value that follows it on the command line.

When Counter.Add is invoked for the first time, the module Counter is loaded and its body is executed. Every call of Counter.Add n increments the variable counter by n. Every call of Counter.Write writes the current value of counter to the screen.

Since a module remains loaded after the execution of its commands, there must be an explicit way to unload it (e.g. when the user wants to substitute the loaded version by a recompiled version.) The Oberon system provides a command to do that.

D2. Dynamic Loading of Modules

A loaded module may invoke a command of a still unloaded module by specifying its name as a string. The specified module is then dynamically loaded and the designated command is executed. Dynamic loading allows the user to start a program as a small set of basic modules and to extend it by adding further modules at run time as the need becomes evident.

A module M0 may cause the dynamic loading of a module M1 without importing it. M1 may of course import and use M0, but M0 need not know about the existence of M1. M1 can be a module that is designed and implemented long after M0.

D3. Garbage Collection

In Oberon-2, the predeclared procedure NEW is used to allocate data blocks in free memory. There is, however, no way to explicitly dispose an allocated block. Rather, the Oberon environment uses a garbage collector to find the blocks that are not used any more and to make them available for allocation again. A block is in use as long as it can be reached from a global pointer variable via a pointer chain. Cutting this chain (e.g., setting a pointer to NIL) makes the block collectable.

A garbage collector frees a programmer from the non-trivial task of deallocating data structures correctly and thus helps to avoid errors. However, it requires information about dynamic data at run time (see D5).

D4. Browser

The interface of a module (the declaration of the exported objects) is extracted from the module by a so-called browser which is a separate tool of the Oberon environment. For example, the browser produces the following interface of the module Trees from Ch. 11 (html2).
DEFINITION Trees; 

  TYPE
    Tree = POINTER TO Node;
    Node = RECORD
      name: POINTER TO ARRAY OF CHAR;
      PROCEDURE (t: Tree) Insert (name: ARRAY OF CHAR);
      PROCEDURE (t: Tree) Search (name: ARRAY OF CHAR): Tree;
      PROCEDURE (t: Tree) Write;
    END;

  PROCEDURE Init (VAR t: Tree);

END Trees.
For a record type, the browser also collects all procedures bound to this type and shows their declaration in the record type declaration.

D5. Run Time Data Structures

Certain information about records has to be available at run time: The dynamic type of records is needed for type tests and type guards. A table with the addresses of the procedures bound to a record is needed for calling them. Finally, the garbage collector needs information about the location of pointers in dynamically allocated records. All that information is stored in so-called type descriptors of which there is one for every record type at run time. The following paragraphs show a possible implementation of type descriptors.

The dynamic type of a record corresponds to the address of its type descriptor. For dynamically allocated records this address is stored in a so-called type tag which precedes the actual record data and which is invisible for the programmer. If t is a variable of type CenterTree (see example in Ch. 6 (html2)) Figure D5.1 shows one possible implementation of the run time data structures.

Fig. D5.1 A variable t of type CenterTree, the record t^ it points to, and its type descriptor

Since both the table of procedure addresses and the table of pointer offsets must have a fixed offset from the type descriptor address, and since both may grow when the type is extended and further procedures and pointers are added, the tables are located at the opposite ends of the type descriptor and grow in different directions.

A type-bound procedure t.P is called as t^.tag^.ProcTab[IndexP]. The procedure table index of every type-bound procedure is known at compile time. A type test v IS T is translated into v^.tag^.BaseTypes[ExtensionLevelT] = TypeDescrAdrT. Both the extension level of a record type and the address of its type descriptor are known at compile time. For example, the extension level of Node is 0 (it has no base type), and the extension level of CenterNode is 1.

  1. N.Wirth, J.Gutknecht: The Oberon System. Software Practice and Experience 19, 9, Sept. 1989
  2. M.Reiser: The Oberon System. User Guide and Programming Manual. Addison-Wesley, 1991
  3. C.Pfister, B.Heeb, J.Templ: Oberon Technical Notes. Report 156, ETH Zürich, March 1991

Previous Section (html2), Next Section, Contents
Adapted to HTML by Jürgen Geßwein; 12. Juni 1995