The Omega Language

The Omega language itself is rather small and simple. Objects, types, inheritance, assignments and messages - that's all that is needed to understand and teach the language proper. Of course, more is needed to actually use Omega in software projects. In particular, it is necessary to understand the key concepts of the library, since virtually all operations are defined in the library, not in the language. And, as Omega programs are developed interactively, it is also necessary to understand the workspace concept and to know how to manage the workspace by means of the programming environment. This section concentrates on the actual language features. The key features of the Omega language are:


Reference Semantics and Dynamic Binding

As Omega is a pure object-oriented language, all data are treated as objects. In contrast to most hybrid languages (such as Oberon-2 [Mössenböck 1991, 1993] and C++), Omega uses reference semantics only. This means that variables never contain objects but rather refer to objects. Per definition, all messages to objects are resolved by means of dynamic binding. In other words, all messages are virtual by default. There are no language elements to define a message as non-virtual. However, the compiler may choose to use static binding where this is possible and may improve the efficiency of a program.


Monomorphic Types

In order to improve the efficiency of operations with elementary data types (such as Integer, Real, Char and Boolean), these types are defined as monomorphic in Omega. It is not allowed to derive a new type from any of these types. Consequently, a variable v of, say, type Integer is guaranteed to always contain an integer value. The compiler makes use of this knowledge to optimize operations with values of elementary types. It is noteworthy that such values are still treated as objects. In particular, Integer is derived from the most general type Object and thus inherits the methods already defined in Object. Moreover, an Integer object can be used wherever an object of type Object is allowed. This is particularly important with the generic container types. For example, a set of integers can be constructed by parameterizing the generic prototype Set with Integer. This is possible because type Integer is a specialization of the most general type, Object.


Messages

The syntax of Omega is similar to that of Smalltalk. In particular, all operations are expressed by unary, binary and keyword messages, and every expression (including assignments and declarations) returns a value. The following examples show some typical Omega expressions.

  x:=y           -- assigns the reference contained in y to x
                 -- and returns this reference.
  x clone        -- unary message; returns a shallow copy of x.
  'X'*5          -- binary message; returns the string "XXXXX".
  arr at:5       -- keyword message; returns the fifth element
                 -- of the array arr.
  arr at:5 put:x -- keyword message; puts x into the fifth element
                 -- of the array arr.
 

 

Declarations

As Omega is a typed language, every variable has to be declared and associated with a data type. There are three kinds of declarations. A fully qualified declaration takes the form v:T:=e, where v is the variable to be declared, T is its (static) type, and e is an expression that determines the variable's initial value. Both the expression and the type can be omitted. The form v:T declares v as of type T and assigns an appropriate initial value to v (zero for numeric types, false for Boolean, the null character for type Char, Nil for other types). When the type is omitted, a declaration takes the form v::=e. In this case, the variable is implicitly declared with the static type of the expression e. The following table shows some typical declarations and explains their effects.

  declaration                type           initial value
  ------------------------------------------------------------------------
  i::=42                     Integer        42
  arr::=Array{integer} copy  Array{Integer} a new, empty array of integers
  s:String                   String         Nil
  c:Integer                  Integer        0
  w:Window:=ZoomWindow copy  Window         a new ZoomWindow object

As illustrated by these examples, the fully qualified form is needed only when the variable and its initial value are to be of different static types. In all other cases, one of the shorter forms can be used. Variables can be declared anywhere within a method. They are typically not introduced at the beginning of a method, but rather at the point where they are first needed.



Conditional Assignments

Assignment compatibility is governed by the usual rules for polymorphism. An object (or, rather, a reference to an object) can be assigned to a variable when the object's types is identical to or a subtype of the variable's type. The possible operations with a variable are determined by the variable's type. The message v m is considered legal when the method m has been defined in the variable's static type T or in a direct or indirect supertype of T. While this rule ensures correct usage of variables, it is sometimes too rigid as it narrows the programmer's view of objects. For example, let the type T1 be a subtype of T with an additional method m1. It would be perfectly legal to assign a T1 object to the variable v of type T (e.g., v:=T1 copy). However, the message v m1 would be rejected by the compiler, because m1 has not been defined for type T. This is correct because it is not statically known that the variable v in fact refers to an object of type T1. A run-time test along with a type cast is therefore required to send the message m1 to the object referred to by v. In Omega, this is done by means of a conditional assignment. In a conditional assignment v:?=e, the expression e need not be statically compatible with the variable v. Instead, the compatibility of e with v is checked at run time. When the dynamic type of e is compatible with the static type of v, the assignment is performed, and the whole expression returns the value true. Otherwise, no assignment takes place, and the value false is returned. To check whether the variable v refers to an object of type T1 and, if so, send the message m1 to this object, the following statement sequence can be used.

  v1:T1;             -- v1 is an auxiliary variable of the static type T1
  (v1:?=v)           -- try to assign v to v1
    ifTrue:[v1 m1];  -- if the assignment was successful,
                     -- send the message m1 to v1

In contrast to type casts in C++, Omega's conditional assignments are type-safe operations. They are possible only because knowledge about the dynamic type of objects (so-called meta-information) is available at run time.



Blocks

Like Smalltalk, Omega uses blocks for flow control. A block is an expression sequence enclosed in brackets. Blocks are first-class objects and can be sent messages and passed as arguments of messages. The message do is used to execute a block object. Blocks are used in Omega for the construction of conditional expressions, short-circuit evaluation of boolean expressions, and exception handling. The example above already shows a typical application of block objects.

In contrast to Smalltalk blocks, Omega blocks can be evaluated only in LIFO order. That is, a block can be executed only as long as the method containing the block is still executing. This is enforced statically by a simple rule: Blocks may be used only as receivers and arguments of messages, but they cannot be assigned to variables.

There is also a generic variant of blocks. So-called actions can be parameterized with an arbitrary type. For example, Action{Integer} represents an action that must be supplied with an integer argument upon activation with the message doWith:. Actions are primarily used for iteration over collections of objects. For example, the code fragmen

  windowList forAll:{w:Window}[w close]

closes all windows contained in the array denoted by windowList.

For the sake of efficiency, both Block and Action are defined as monomorphic in Omega. Although all control flow constructs are implemented as methods, the compiler can perform various optimizations on them.


Methods

A method consists of a sequence of expressions enclosed in square brackets. The expressions are executed from left to right, and the result of the last expression determines the value to be returned by the method. Within a method, the predefined identifier self denotes the receiver of the method. The following example shows the implementation of the Integer method factorial for computing the factorial of the receiver.

  [ fact::=1;
    2 to:self do:{i:Integer}[fact:=fact*i];
    fact ]

Methods with arguments must be preceded with the declaration of their formal arguments within braces. The following example shows the implementation of the Integer method to:do: that was used in the above method.

  {limit:Integer; act:Action{Integer}}
  [ i::=self;
    [i<=limit] whileTrue: [act doWith:i; i:=i+1];
    self ]

The formal arguments limit and act correspond to the arguments passed after the keywords to: and do:, respectively. They are treated as read-only within a method. It is thus not possible to return an object through a method argument. If a method is supposed to return an object, it must be returned as the method result. If a method has no meaningful result, it returns the receiver by convention (hence the identifier self at the end of the above method).



Garbage Collection

An important consequence of reference semantics is that Omega programs work with dynamic objects only. During execution of a program, many objects are created. To get rid of obsolete objects, the run-time environment contains a garbage collector. In this way, a high degree of safety is achieved, and the programmer is relieved from the tedious task of keeping track of objects and determining when they can safely be disposed of.


Previous Section:

Following Sections:


This page has been visited times.