The language: Oberon in one chapter
Oberon is Wirth's last language, heir to Pascal and Modula-2. Its 2007 revision, the one this system runs, is meant to be a language that fits in one head entire: thirty-three keywords and a little over forty built-in names. For comparison, C++ has about a hundred keywords.
What follows is not a full description but a working minimum: enough to read the system's sources and write your own modules.
A whole module
Here is a real module from this system's disk — it blinks an LED on the board:
MODULE Blink;
IMPORT SYSTEM, Oberon;
VAR z: INTEGER;
T: Oberon.Task;
PROCEDURE Run*;
BEGIN Oberon.Install(T)
END Run;
PROCEDURE Stop*;
BEGIN Oberon.Remove(T)
END Stop;
PROCEDURE Tick;
BEGIN z := 1 - z; LED(z)
END Tick;
BEGIN z := 0; T := Oberon.NewTask(Tick, 500)
END Blink.
Almost the whole shape of the language is visible here.
A module is the unit of compilation and the unit of loading. Its body —
between the last BEGIN and END — runs once, when it is loaded.
A star after a name means export. Run* is visible from outside, Tick is
not. That is the entire visibility mechanism: no public, no private, no
header files, no export lists.
The module's name is repeated after the final END and followed by a full
stop. The compiler checks it.
Keywords
All thirty-three:
ARRAY BEGIN BY CASE CONST DIV DO ELSE ELSIF END FALSE FOR IF IMPORT
IN IS MOD MODULE NIL OF OR POINTER PROCEDURE RECORD REPEAT RETURN
THEN TO TRUE TYPE UNTIL VAR WHILE
Note what is absent: class, try, catch, throw, template,
interface, async, private. None of it is in the language — and that is a
deliberate choice, not poverty.
Types
The basic ones: INTEGER, REAL, BOOLEAN, CHAR, BYTE, SET.
SET is a set of 32 elements, one bit each, with + (union), *
(intersection), - (difference) and the IN test. Cheap and very expressive;
the system uses it constantly, for the state of the mouse buttons among other
things.
The compound ones:
TYPE
Name = ARRAY 32 OF CHAR;
Point = RECORD x, y: INTEGER END;
Node = POINTER TO NodeDesc;
NodeDesc = RECORD key: INTEGER; next: Node END;
Shape = RECORD x, y: INTEGER END;
Circle = RECORD (Shape) r: INTEGER END; (* record extension *)
Records extend — that is all the inheritance the language has. A run-time type
test is IS, a cast is v(Circle). There are no virtual methods: instead a
variable of procedure type is placed in the record. That is exactly how every
"object" in this system is built.
Arrays come with a fixed length (ARRAY 32 OF CHAR) or open, only as procedure
parameters (ARRAY OF CHAR); LEN gives their length.
Statements
Assignment is :=, comparison = and #. Integer division is DIV and
MOD, real division /.
IF x > 0 THEN y := 1 ELSIF x < 0 THEN y := -1 ELSE y := 0 END;
WHILE i < n DO s := s + a[i]; INC(i) END;
REPEAT ch := Read() UNTIL ch = 0X;
FOR i := 0 TO n-1 DO a[i] := 0 END;
CASE ch OF
"a".."z": Small(ch)
| "0".."9": Digit(ch)
ELSE Other(ch)
END
Every construct closes with its own END. There are no braces, no dangling
else, and the semicolon is a separator rather than a terminator: it is
not wanted before END.
Procedures
PROCEDURE Add(a, b: INTEGER): INTEGER;
BEGIN RETURN a + b
END Add;
PROCEDURE Swap(VAR a, b: INTEGER);
VAR t: INTEGER;
BEGIN t := a; a := b; b := t
END Swap;
A parameter marked VAR is passed by reference, one without by value. Local
declarations go between the heading and BEGIN. Nested procedures are allowed
and see the enclosing procedure's variables.
RETURN in a function is the last statement — there is no leaving from the
middle. This is inconvenient right up to the moment you have to read somebody
else's code.
Built-in procedures
There are a little over forty, and almost all are one line:
INC DEC | increment, decrement |
INCL EXCL | add, remove a set element |
LEN | length of an open array |
ORD CHR | character ↔ code |
ABS ODD | magnitude, oddness |
FLOOR FLT | real ↔ integer |
PACK UNPK | take a real apart and put it back together |
NEW | allocate for a pointer |
ASSERT | a check; a trap if false |
LED | light the LEDs on the board |
The pseudo-module SYSTEM stands apart: ADR, VAL, GET, PUT, COPY,
BIT, H, SIZE. It is the door out of a type-safe language, and the drivers
and the garbage collector are written through it. A module that imports
SYSTEM is marked specially by the compiler, and it shows in its source on the
first line.
What the language lacks, and why
No exceptions. A run-time error is a trap: it prints the position and
stops the command. A program does not wrap try around everything.
No overloading — neither of operators nor of procedure names. A name means exactly one thing.
No generics. A container is written either for a concrete type or through record extension.
No preprocessor, macros or conditional compilation. The text you see is the text that compiles.
No header files. The compiler extracts a module's interface from the module itself; how, is in the chapter on modules.
The language was designed under one constraint: a compiler for it had to be writable by one person in a foreseeable time. The check is simple — the whole Oberon compiler is four modules and a hundred and nine kilobytes of source. Which modules those are is taken apart in the chapter on the compiler.