Modula-2 for C/C++/Java/C# programmers

Content:

  1. Introduction
  2. History
  3. Compilers
  4. Source format
  5. Data types and constants
  6. Operators and functions
  7. Conditional statements
  8. Loops
  9. Arrays, records, scalars and subranges
  10. Procedures
  11. IO
  12. Pointer and dynamic memory
  13. Set
  14. Modules
  15. Various

Introduction:

I have never actually coded in Modula-2, but I have read about it. I first started reading about it like 30 years ago and I was impressed. I already knew Fortran, Pascal, C and C++ at the time, but I liked what I read about Modula-2.

In my opinion Modula-2 is the finest procedural language ever designed. One of the last and the finest of a long line of languages until the object oriented languages took over the world.

Modula-2 was never widely used in the IT industry. Most usage was in the embedded field. The most known cases are car ECU's back in the 80's and 90's and the russian navigation system GLONASS.

This article will explain Modula-2 for C/C++/Java/C# programmers.

Pascal programmers should go here.

History:

Modula-2 is invented by Niklaus Wirth and should be understood in the context of his other language inventions.

Language Year Notes
ALGOL W 1966 Based on ALGOL 60. With Tony Hoare.
Pascal 1970 Based on ALGOL W. Intended for teaching programming.
Modula-2 1978 Based on Pascal. Intended for actual software development.
Oberon 1987 Based on Modula-2. Somewhat object oriented.
Oberon-2 1991 Based on Oberon. Fully object oriented. With Hanspeter Mössenböck.

There are two standards for Modula-2:

PIM
Defined in "Programming in Modula-2" by Wirth. PIM2 = 1983 edition. PIM3 = 1985 edition. PIM4 = 1988 edition.
ISO
Defined in ISO 10514 standard (1996).

Compilers:

Most Modula-2 compilers were commercial.

But a few free exist including:

All examples here are tested with GNU Modula-2.

GNU Modula-2 can be installed on Ubuntu Linux with:

sudo apt install gm2

Code is compiled with:

gm2 -c -fiso Foobar.mod

for modules and:

gm2 -fiso Foobar.mod -o Foobar

for main programs.

Source format:

Modula-2 is a free format language like its predecsssors ALGOL and Pascal (and like almost all newer languages).

Statements are seperated by semicolon.

Modula-2 use:

keyword ...
   ...
END;

for blocks instead of {}.

Modula-2 is case sensitive.

All keywords are uppercase.

Data types and constants:

Modula-2 has the following data types:

Modula-2 type Meaning C/C++ equivalent Java equivalent C# equivalent
INTEGER signed integer (16 or 32 bit) short int or int or long int short or int short or int
CARDINAL unsigned integer (16 or 32 bit) unsigned short int or unsigned int or unsigned long int (none) ushort or uint
BOOLEAN true or false bool boolean bool
CHAR char (8 or 16 bit) char or wchar_t byte or char byte or char
REAL 32 bit floating point float float float
LONGREAL 64 bit floating point double double double
COMPLEX 64 bit complex = 32 bit real part + 32 bit imaginary part (none) (none) (none)
LONGCOMPLEX 128 bit complex = 64 bit real part + 64 bit imaginary part (none) (none) (none)

Strings are not an independent type but are ARRAY OF CHAR.

Be careful to make those ARRAY [0..n] OF CHAR and not ARRAY [1..n] OF CHAR!

Constants are defined via CONST declaration.

Variables are defined via VAR declation.

Example:

MODULE VarEx;

FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteInt, WriteCard;
FROM SRealIO IMPORT WriteReal;

CONST
    c1 = 123;
    c2 = 'ABC';

VAR
    v1 : INTEGER;
    v2 : INTEGER;
    v3 : CARDINAL;
    v4 : BOOLEAN;
    v5 : REAL;
    v6 : REAL;

BEGIN
    WriteInt(c1, 10);
    WriteLn;
    WriteString(c2);
    WriteLn;
    v1 := 123;
    v2 := -123;
    v3 := 123;
    v4 := TRUE;
    v5 := 123.456;
    v6 := -123.456;
    WriteInt(v1, 10);
    WriteLn;
    WriteInt(v2, 10);
    WriteLn;
    WriteCard(v3, 10);
    WriteLn;
    IF v4 THEN
        WriteString('True');
    ELSE
        WriteString('False');
    END;
    WriteLn;
    WriteReal(v5, 10);
    WriteLn;
    WriteReal(v6, 10);
    WriteLn;
END VarEx.

Operators and functions:

Modula-2 has the normal operators:

Modula-2 Meaning C/C++/Java/C#
+ add +
- subtract -
* multiply *
/ divide /
MOD modulus %
= equals ==
<> not equals !=
< less than <
> greater than >
<= less than or equals <=
>= greater than or equals >=
AND logical and &&
OR logical or ||
NOT logical not !

Example:

MODULE OpEx;

FROM STextIO IMPORT WriteLn;
FROM SWholeIO IMPORT WriteInt;
FROM SRealIO IMPORT WriteReal;

VAR
    n, m : INTEGER;
    x, y : REAL;

BEGIN
    n := 7;
    m := 5;
    WriteInt(n + m, 4);
    WriteLn;
    WriteInt(n - m, 4);
    WriteLn;
    WriteInt(n * m, 4);
    WriteLn;
    WriteInt(n / m, 4);
    WriteLn;
    WriteInt(n MOD m, 4);
    WriteLn;
    x := 2.5;
    y := 1.25;
    WriteReal(x + y, 6);
    WriteLn;
    WriteReal(x - y, 6);
    WriteLn;
    WriteReal(x * y, 6);
    WriteLn;
    WriteReal(x / y, 6);
    WriteLn;
END OpEx.

Note that INTEGER and REAL/LONGREAL cannot be mixed in expressions. Explicit conversion is required.

A few practical functions:

INT(x)
truncate REAL/LONGREAL to INTEGER
TRUNC(v)
truncate REAL/LONGREAL to CARDINAL
FLOAT(x)
convert INTEGER/CARDINAL to REAL
LFLOAT(x)
convert INTEGER/CARDINAL to LONGREAL
INC(v)
v = v + 1
DEC(v)
v = v - 1
ORD(v)
CHAR to INTEGER
CHR(v)
INTEGER TO CHAR
HIGH(a)
upper index of array

Example:

MODULE FuncEx;

FROM STextIO IMPORT WriteChar, WriteLn;
FROM SWholeIO IMPORT WriteInt;
FROM SRealIO IMPORT WriteReal;

VAR
    iv : INTEGER;
    xv : REAL;
    c : CHAR;

BEGIN
    iv := 2;
    xv := 2.75;
    c := 'A';
    WriteInt(iv, 4);
    WriteLn;
    INC(iv);
    WriteInt(iv, 4);
    WriteLn;
    DEC(iv);
    WriteInt(iv, 4);
    WriteLn;
    WriteInt(iv + INT(xv), 4);
    WriteLn;
    WriteReal(FLOAT(iv) + xv, 6);
    WriteLn;
    WriteChar(c);
    WriteLn;
    iv := ORD(c);
    INC(iv);
    c := CHR(iv);
    WriteChar(c);
    WriteLn;
END FuncEx.

String procedures are defined in module Strings.

Some common procedures:

Modula-2 functionality C equivalent C++ equivalent Java equivalent C# equivalent
s2 := s1; Assign strcpy(s2, s1); s2 = s1; s2 = s1; s2 = s1;
len := Length(s); Length of s len = strlen(s); len = s.length(); len = s.length(); len = s.Length();
Append(s1, s2); Append s1 to s2 strcat(s2, s1); s2 = s2 + s1; s2 = s2 + s1; s2 = s2 + s1;
Extract(s1, startix, len, s2); Extract substring from s1 to s2 strncpy(s2, s1 + startix, len); s2 = s1.substr(startix, len) s2 = s1.substring(startix, startix + len); s2 = s1.Substring(startix, len);
rel = Compare(s1, s2) Compare s1 with s2 rel = strcmp(s1, s2); rel = s1.compare(s2); rel = s1.compareTo(s2); rel = s1.CompareTo(s2);
FindNext(s1, s2, startix, fnd, fndix); Find s1 in s2 p = strstr(s2 + startix, s1); fndix = s2.find(s1, startix); fndix = s2.indexOf(s2, stratix); fndix = s2.IndexOf(s2, stratix);

Example:

MODULE StrEx;

FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteInt, WriteCard;
FROM Strings IMPORT Length, Capitalize, Extract, Insert, Delete, Replace, Append, Concat, Equal, Compare, equal, less, greater, FindNext;

VAR
    s, s2 : ARRAY [0..9] OF CHAR;
    ix : CARDINAL;
    fnd : BOOLEAN;

BEGIN
    s := 'Hello';
    WriteString('s: ');
    WriteString(s);
    WriteLn;

    WriteString('Length(s): ');
    WriteInt(Length(s), 4);
    WriteLn;

    s2 := s;
    Capitalize(s2);
    WriteString('s2 after Capitalize(s2): ');
    WriteString(s2);
    WriteLn;

    Extract(s, 1, 3, s2);
    WriteString('s2 after Extract(s, 1, 3, s2): ');
    WriteString(s2);
    WriteLn;

    s2 := s;
    Insert('xx', 2, s2);
    WriteString('s2 after Insert(xx, 2, s2): ');
    WriteString(s2);
    WriteLn;

    s2 := s;
    Delete(s2, 2, 2);
    WriteString('s2 after Delete(s2, 2, 2): ');
    WriteString(s2);
    WriteLn;
    
    s2 := s;
    Replace('xx', 2, s2);
    WriteString('s2 after Replace(xx 2, s2): ');
    WriteString(s2);
    WriteLn;
    
    s2 := s;
    Append('xx', s2);
    WriteString('s2 after Append(xx, s2): ');
    WriteString(s2);
    WriteLn;
    
    Concat(s, 'xx', s2);
    WriteString('s2 after Concat(s, xx, s2): '); 
    WriteString(s2);
    WriteLn;
    
    WriteString('Equal(s, Hello): ');
    IF Equal(s, 'Hello') THEN
       WriteString('Equal');
       WriteLn;
    ELSE
       WriteString('Not equal');
       WriteLn;
    END;
    
    WriteString('Equal(s, xx): ');
    IF Equal(s, 'xx') THEN
       WriteString('Equal');
       WriteLn;
    ELSE
       WriteString('Not equal');
       WriteLn;
    END;
    
    WriteString('Compare(s, Hello): ');
    IF Compare(s, 'Hello') = equal THEN
       WriteString('Equal');
       WriteLn;
    ELSIF Compare(s, 'Hello') = less THEN
       WriteString('Less');
       WriteLn;
    ELSIF Compare(s, 'Hello') = greater THEN
       WriteString('Greater');
       WriteLn;
    END;
    
    WriteString('Compare(s, xx): ');
    IF Compare(s, 'xx') = equal THEN
       WriteString('Equal');
       WriteLn;
    ELSIF Compare(s, 'xx') = less THEN
       WriteString('Less');
       WriteLn;
    ELSIF Compare(s, 'xx') = greater THEN
       WriteString('Greater');
       WriteLn;
    END;
    
    WriteString('FindNext(el, s, 0, fnd, ix): ');
    FindNext('el', s, 0, fnd, ix);
    IF fnd THEN
       WriteString('Found at ');
       WriteCard(ix, 1);
       WriteLn;
    ELSE
       WriteString('Not found');
       WriteLn;
    END;
    
    WriteString('FindNext(le, s, 0, fnd, ix): ');
    FindNext('le', s, 0, fnd, ix);
    IF fnd THEN
       WriteString('Found at ');
       WriteCard(ix, 1);
       WriteLn;
    ELSE
       WriteString('Not found');
       WriteLn;
    END;
END StrEx.

Output:

s: Hello
Length(s):   +5
s2 after Capitalize(s2): HELLO
s2 after Extract(s, 1, 3, s2): ell
s2 after Insert(xx, 2, s2): Hexxllo
s2 after Delete(s2, 2, 2): Heo
s2 after Replace(xx 2, s2): Hexx
s2 after Append(xx, s2): Helloxx
s2 after Concat(s, xx, s2): Helloxx
Equal(s, Hello): Equal
Equal(s, xx): Not equal
Compare(s, Hello): Equal
Compare(s, xx): Less
FindNext(el, s, 0, fnd, ix): Found at 1
FindNext(le, s, 0, fnd, ix): Not found

These procedures are a bit cumbersome by modern standards, but they can do what is needed.

Conditional statements:

Modula-2 has of course IF and CASE statement for conditional execution.

Modula-2 C/C++/Java/C#
IF condition THEN
    ...
END;
if(condition) {
    ...
}
IF condition THEN
    ...
ELSE
    ...
END;
if(condition) {
    ...
} else {
    ...
}
IF condition THEN
    ...
ELSIF othercondition THEN
    ...
ELSE
    ...
END;
if(condition) {
    ...
} else if(othercondition) {
    ...
} else {
    ...
}
CASE expression OF
    value_1 : ... |
    value_2 : ... |
    ...
    value_n : ...
ELSE
    ...
END;
switch(expression) {
    case value_1 :
        ...
        break;
    case value_2 :
        ...
        break;
    ...
    case value_n :
        ...
        break;
    default:
        ...
        break;
}

Example:

MODULE CondEx;

FROM STextIO IMPORT WriteString, WriteLn;

VAR
    v : INTEGER;

BEGIN
    v := 2;
    IF v = 1 THEN
        WriteString('v = 1');
        WriteLn;
    END;
    IF v = 1 THEN
        WriteString('v = 1');
        WriteLn;
    ELSE
        WriteString('v not 1');
        WriteLn;
    END;
    IF v = 1 THEN
        WriteString('v = 1');
        WriteLn;
    ELSIF v = 2 THEN
        WriteString('v = 2');
        WriteLn;
    ELSE
        WriteString('v not 1 or 2');
        WriteLn;
    END;
    CASE v OF
        1 : WriteString('v = 1');
            WriteLn; |
        2 : WriteString('v = 2');
            WriteLn; |
        3 : WriteString('v = 3');
            WriteLn;
        ELSE
            WriteString('v not 1 or 2');
            WriteLn;
    END;
END CondEx.

Loops:

Modula-2 has the 3 classic loops (counter, top test and bottom test) plus a general loop.

Modula-2 C/C++/Java/C#
FOR counter := start TO finish DO
    ...
END;
or:
FOR counter := start TO finish BY step DO
    ...
END;
for(int counter = start; counter <= finish; counter++) {
    ...
}
or:
for(int counter = start; counter <= finish; counter = counter + step) {
    ...
}
WHILE condition DO
    ...
END;
while(condition) {
    ...
}
REPEAT
    ...
UNTIL condition;
do {
    ...
} while(condition);
LOOP
    ...
    IF condition THEN
        EXIT;
    END;
    ...
END;
for(;;) {
    ...
    if(condition) break;
    ...
}
or:
while(true) {
    ...
    if(condition) break;
    ...
}

Example:

MODULE LoopEx;

FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteInt;

VAR
    i : INTEGER;

BEGIN
    FOR i := 1 TO 3 DO
        WriteInt(i,1);
        WriteLn;
    END;
    FOR i := 1 TO 5 BY 2 DO
        WriteInt(i,1);
        WriteLn;
    END;
    i := 1;
    WHILE i <= 3 DO
        WriteInt(i,1);
        WriteLn;
        INC(i);
    END;
    i := 1;
    REPEAT
        WriteInt(i,1);
        WriteLn;
        INC(i);
    UNTIL i > 3;
    i := 1;
    LOOP
        WriteInt(i,1);
        WriteLn;
        IF i >= 3 THEN
            EXIT;
        END;
        INC(i);
    END;
END LoopEx.

Arrays, records, scalars and subranges:

Arrays:

Declaration of arrays:

VAR
    varname : ARRAY [0..n] OF typeofvar;

or:

TYPE
    typename = ARRAY [lowix..highix] OF typeofvar;
...
VAR
    varname : typename;

And elements are referenced as:

varname[index]

Example:

MODULE ArrEx;

FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteInt;

CONST
    dim = 10;

VAR
    i : INTEGER;
    a : ARRAY [1..dim] OF INTEGER;

BEGIN
    FOR i := 1 TO dim DO
        a[i] := i * i;
    END;
    FOR i := 1 TO dim DO
        WriteInt(i,3);
        WriteString(' : ');
        WriteInt(a[i], 3);
        WriteLn;
    END;
END ArrEx.

Records:

Declaration of records:

TYPE
    typename = RECORD
                   fieldname1 : fieldtype1;
                   fieldname2 : fieldtype2;
                   ...
                END;
...
VAR
    varname : typename;

And fields are referenced as:

varname.fieldname

Example:

MODULE RecEx;

FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteInt;

CONST
    namlen = 32;
    maxitem = 3;

TYPE
   namstr = ARRAY [1..namlen] OF CHAR;
   item = RECORD
              id : INTEGER;
              name : namstr;
          END;
   itemlist = ARRAY [1..maxitem] OF item;

VAR
    i : INTEGER;
    lst : itemlist;

BEGIN
    lst[1].id := 1;
    lst[1].name := 'A';
    lst[2].id := 2;
    lst[2].name := 'BB';
    lst[3].id := 3;
    lst[3].name := 'CCC';
    FOR i := 1 TO maxitem DO
        WriteInt(lst[i].id, 4);
        WriteString(' ');
        WriteString(lst[i].name);
        WriteLn;
    END;
END RecEx.

Modula-2 has a WITH statement like Pascal and Basic so the example can be written as:

MODULE Rec2Ex;

FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteInt;

CONST
    namlen = 32;
    maxitem = 3;

TYPE
   namstr = ARRAY [1..namlen] OF CHAR;
   item = RECORD
              id : INTEGER;
              name : namstr;
          END;
   itemlist = ARRAY [1..maxitem] OF item;

VAR
    i : INTEGER;
    lst : itemlist;

BEGIN
    WITH lst[1] DO
        id := 1;
        name := 'A';
    END;
    WITH lst[2] DO
        id := 2;
        name := 'BB';
    END;
    WITH lst[3] DO
        id := 3;
        name := 'CCC';
    END;
    FOR i := 1 TO maxitem DO
        WITH lst[i] DO
            WriteInt(id, 4);
            WriteString(' ');
            WriteString(name);
            WriteLn;
        END;
    END;
END Rec2Ex.

Scalars:

Modula-2 enums are called scalars.

TYPE
    typename = (val1, val2, ...);
...
VAR
    varname : typename;

Subranges:

Modula-2 like Pascal and Ada (but unlike C/C++/Java/C#) support subranges of integers.

TYPE
    typename = [startval..endval];
...
VAR
    varname : typename;

Procedures:

Modula-2 ofcourse have routines - they are called procedures.

Definition:

PROCEDURE procname(arg1 : type1, arg2 : type2, ...);

BEGIN
    ...
END procname;

and:

PROCEDURE procname(arg1 : type1, arg2 : type2, ...) : rettype;

BEGIN
    ...
    RETURN(retvalue);
END procname;

Call:

procname(arg1, arg2, ...);

and:

res := procname(arg1, arg2, ...);

Arguments are default IN (by value) but can be made INOUT/OUT (by reference) by prefixing with VAR keyword.

Note that procedures can be nested.

Example:

MODULE ProcEx;

FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteInt;

PROCEDURE DoSomething(s : ARRAY OF CHAR);

BEGIN
    WriteString('DoSomething called with : ');
    WriteString(s);
    WriteLn;
END DoSomething;

PROCEDURE Fac(n : INTEGER) : INTEGER;

BEGIN
    IF n < 2 THEN
        RETURN(1);
    ELSE
        RETURN(n * Fac(n - 1));
    END;
END Fac;

PROCEDURE Add(VAR v : INTEGER; dv : INTEGER);

BEGIN
    v := v + dv;
END Add;

PROCEDURE P1;

PROCEDURE P;

BEGIN
    WriteString('P1.P');
    WriteLn;
END P;

BEGIN
    P;
END P1;

PROCEDURE P2;

PROCEDURE P;

BEGIN
    WriteString('P2.P');
    WriteLn;
END P;

BEGIN
    P;
END P2;

VAR
    i, v : INTEGER;

BEGIN
    DoSomething('ABC');
    DoSomething('XYZ');
    FOR i := 1 TO 5 DO
        WriteInt(i, 2);
        WriteString(' : ');
        WriteInt(Fac(i), 4);
        WriteLn;
    END;
    v := 123;
    Add(v, 1);
    WriteInt(v, 4);
    WriteLn;
    P1;
    P2;
END ProcEx.
+1 :   +1
+2 :   +2
+3 :   +6
+4 :  +24
+5 : +120
+124
P1.P
P2.P

IO:

We have already seen Modula-2's console IO many times in previous sections.

It is a bit verbose but also fully type safe and probably very efficient.

Note that positive integers are written with explicit '+'. That was a common practice in the old days.

File IO follows the same model.

Example:

MODULE FileEx;

FROM IOChan IMPORT ChanId, ReadResult;
FROM SeqFile IMPORT OpenRead, OpenWrite, Close, read, write;
FROM TextIO IMPORT ReadString, SkipLine, WriteString, WriteLn;
FROM WholeIO IMPORT WriteCard;
FROM ChanConsts IMPORT OpenResults, opened;
FROM IOConsts IMPORT endOfInput;

VAR
    f1, f2 : ChanId;
    res : OpenResults;
    line : ARRAY [1..132] OF CHAR;
    lno : CARDINAL;

BEGIN
    OpenRead(f1, 'z1.txt', read, res);
    IF res <> opened THEN
        HALT;
    END;
    OpenWrite(f2, 'z2.txt', write, res);
    IF res <> opened THEN
        HALT;
    END;
    lno := 0;
    LOOP
        ReadString(f1, line);
        IF ReadResult(f1) = endOfInput THEN
            EXIT;
        END;
        SkipLine(f1);
        INC(lno);
        WriteCard(f2, lno, 1);
        WriteString(f2, ': ');
        WriteString(f2, line);
        WriteLn(f2);
    END;
    Close(f1);
    Close(f2);
END FileEx.

Output:

arne@ubuntu:~/art/m2$ cat z1.txt
A
BB
CCC
arne@ubuntu:~/art/m2$ cat z2.txt
1: A
2: BB
3: CCC

Note that all examples use ISO modules. PIM modules have different names. But the IO paradigm is always the same.

Pointer and dynamic memory:

Modula-2 has type safe pointers.

Declaration:

TYPE
    foobar = ...
    foobarptr = POINTER TO foobar;
...
VAR
    fbp : foobarptr;

Allocation:

    NEW(fbp);

Usage:

   ... fbp^ ...;

Dellocation:

    DISPOSE(fbp);

Note that there actually two levels of allocation/deallocation procedures in Modula-2:

I will be using NEW and DISPOSE.

But note that NEW and DISPOSE require ALLOCATE and DEALLOCATE To be imported!

Example:

MODULE PtrDynEx;

FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteCard;

FROM Storage IMPORT ALLOCATE, DEALLOCATE;

CONST
    namelen = 31;

TYPE
    namestr = ARRAY [0..namelen] OF CHAR;
    itemptr = POINTER TO item;
    item = RECORD
               id : CARDINAL;
               name : namestr;
               next : itemptr;
           END;

PROCEDURE create(id : CARDINAL; name : namestr) : itemptr;

VAR
    firstitem : itemptr;

BEGIN
    NEW(firstitem);
    firstitem^.id := id;
    firstitem^.name := name;
    firstitem^.next := NIL;
    RETURN(firstitem);
END create;

PROCEDURE add(first : itemptr; id : CARDINAL; name : namestr);

VAR
    curr : itemptr;
    additem : itemptr;

BEGIN
    curr := first;
    WHILE curr^.next <> NIL DO
        curr := curr^.next;
    END;
    NEW(additem);
    additem^.id := id;
    additem^.name := name;
    additem^.next := NIL;
    curr^.next := additem;
END add;

PROCEDURE dump(first : itemptr);

VAR
    curr : itemptr;

BEGIN
    curr := first;
    WHILE curr <> NIL DO
        WriteCard(curr^.id, 1);
        WriteString(' : ');
        WriteString(curr^.name);
        WriteLn;
        curr := curr^.next;
    END;
END dump;

PROCEDURE cleanup(curr : itemptr);

BEGIN
    IF curr^.next <> NIL THEN
        cleanup(curr^.next);
    END;
    DISPOSE(curr);
END cleanup;

VAR
    first : itemptr;

BEGIN
    first := create(1, 'A A');
    add(first, 2, 'B B');
    add(first, 3, 'C C');
    dump(first);
    cleanup(first);
END PtrDynEx.

Output:

1 : A A
2 : B B
3 : C C

Set:

Sets are builtin to Modula-2 (like in Pascal).

Modula-2 functionality C equivalent C++ equivalent Java equivalent C# equivalent
SET OF t Specific SET type N/A Set<T> Set<T> / HashSet<T> ISet<T> / HashSet<T>
a := a + b; add all elements in set b to set a N/A (can be done via insert method) a.addAll(b); a.UnionWith(b);
a := a - b; remove all elements in set b from set a N/A (can be done via erase method) a.removeAll(b); a.ExceptWith(b);
a = a * b; remove all elements in set a that are not in set b N/A N/A N/A c = a.IntersectWith(b);
k in a test if element k is in set a N/A (can be done via find or count method) a.contains(k) a.Contains(k)

Example:

MODULE SetEx;

FROM STextIO IMPORT WriteString, WriteLn;

TYPE
    myscalar = (s1, s2, s3, s4, s5);
    myset = SET OF myscalar;

PROCEDURE Dump(lbl : ARRAY OF CHAR; ms : myset);

BEGIN
    WriteString(lbl);
    WriteString(':');
    IF s1 IN ms THEN
        WriteString(' ');
        WriteString('s1');
    END;
    IF s2 IN ms THEN
        WriteString(' ');
        WriteString('s2');
    END;
    IF s3 IN ms THEN
        WriteString(' ');
        WriteString('s3');
    END;
    IF s4 IN ms THEN
        WriteString(' ');
        WriteString('s4');
    END;
    IF s5 IN ms THEN
        WriteString(' ');
        WriteString('s5');
    END;
    WriteLn;
END Dump;

VAR
    x, y, z : myset;

BEGIN
    x := myset{ s1..s3, s5 };
    Dump('x', x);
    y := myset{ s2..s4 };
    Dump('y', y);
    z := x + y;
    Dump('x+y', z);
    z := x - y;
    Dump('x-y', z);
    z := x * y;
    Dump('x*y', z);
END SetEx.

Output:

x: s1 s2 s3 s5
y: s2 s3 s4
x+y: s1 s2 s3 s4 s5
x-y: s1 s5
x*y: s2 s3

Modules:

The module concept is what really separate Modula-2 from its predecessor Pascal.

(standard Pascal does not have such a feature, but some implementations like Delphi and VMS Pascal have similar features).

We have already seen lots of imports of builtin library modules in all examples. But now we will look at how to create our own library modules.

It is easiest illustrated with an example.

Let us first define a library:

MyLib.def:

DEFINITION MODULE MyLib;

TYPE
    r2 = RECORD
             v : INTEGER;
         END;

PROCEDURE P2(VAR r : r2);

END MyLib.

MyLib.mod:

IMPLEMENTATION MODULE MyLib;

TYPE
    r1 = RECORD
             v : INTEGER;
         END;

PROCEDURE P1(VAR r : r1);

BEGIN
    r.v := 123;
END P1;

PROCEDURE P2(VAR r : r2);

VAR
    temp : r1;

BEGIN
    P1(temp);
    r.v := temp.v;
END P2;

BEGIN
END MyLib.

The relationship with OOP (Object Oriented Programming) principles should be obvious:

The library can be used in two different ways.

Using qualified names:

MODULE Mod1Ex;

FROM STextIO IMPORT WriteLn;
FROM SWholeIO IMPORT WriteInt;

IMPORT MyLib;

VAR
    r : MyLib.r2;

BEGIN
    MyLib.P2(r);
    WriteInt(r.v, 4);
    WriteLn;
END Mod1Ex.

Importing names:

MODULE Mod2Ex;

FROM STextIO IMPORT WriteLn;
FROM SWholeIO IMPORT WriteInt;

FROM MyLib IMPORT P2, r2;

VAR
    r : r2;

BEGIN
    P2(r);
    WriteInt(r.v, 4);
    WriteLn;
END Mod2Ex.

Using qualified names is better avoiding name conflicts, but importing names gives more readable code.

Encapsulation can be further improved by the fact that the type in the definition can be opaque (assuming it is a pointer in the implementation).

Let us take a simple Java example.

Acc.java:

public class Acc {
    private int v;
    public Acc() {
        v = 0;
    }
    public void inc() {
        v++;
    }
    public int get() {
        return v;
    }
}

Test.java:

public class Test {
    public static void main(String[] args) {
        Acc a = new Acc();
        for(int i = 0; i < 3; i++) {
            a.inc();
        }
        System.out.println(a.get());
    }
}

The variable v is private and not accessible outside Acc class.

Now we want to do the same in Modula-2.

Acc.def:

DEFINITION MODULE Acc;

TYPE
    accptr;

PROCEDURE create() : accptr;

PROCEDURE inc(a : accptr);

PROCEDURE get(a : accptr) : CARDINAL;

PROCEDURE free(a : accptr);

END Acc.

Acc.mod:

IMPLEMENTATION MODULE Acc;

FROM Storage IMPORT ALLOCATE, DEALLOCATE;

TYPE
    accrec = RECORD
                 v : CARDINAL;
             END;
    accptr = POINTER TO accrec;

PROCEDURE create() : accptr;

VAR
    a : accptr;

BEGIN
    NEW(a);
    a^.v := 0;
    RETURN(a);
END create;

PROCEDURE inc(a : accptr);

BEGIN
    INC(a^.v);
END inc;

PROCEDURE get(a : accptr) : CARDINAL;

BEGIN
    RETURN(a^.v);
END get;

PROCEDURE free(a : accptr);

BEGIN
    DISPOSE(a);
END free;

BEGIN
END Acc.

Test.mod:

MODULE Test;

FROM STextIO IMPORT WriteLn;
FROM SWholeIO IMPORT WriteCard;

IMPORT Acc;
FROM Acc IMPORT accptr, inc, get, free;

VAR
    a : accptr;
    i : INTEGER;

BEGIN
    a := Acc.create();
    FOR i := 1 TO 3 DO
        inc(a);
    END;
    WriteCard(get(a), 1);
    WriteLn;
    free(a);
END Test.

In C the same just in a sligthly less type safe way could be done via a typedef and void pointer.

acc.h:

typedef void *accptr;

void acc_create(accptr **ctx);

void acc_inc(accptr *ctx);

int acc_get(accptr *ctx);

void acc_free(accptr *ctx);

acc.c:

#include <stdlib.h>

#include "acc.h"

struct acc
{
    int v;
};

void acc_create(accptr **ctx)
{
    struct acc *p;
    p = malloc(sizeof(struct acc));
    p->v = 0;
    *ctx = (void *)p;
}

void acc_inc(accptr *ctx)
{
    struct acc *p;
    p = (struct acc *)ctx;
    p->v++;
}

int acc_get(accptr *ctx)
{
    struct acc *p;
    p = (struct acc *)ctx;
    return p->v++;
}


void acc_free(accptr *ctx)
{
    struct acc *p;
    p = (struct acc *)ctx;
    free(p);
}

test.c:

#include <stdio.h>

#include "acc.h"

int main()
{
    accptr *ap;
    int i;
    acc_create(&ap);
    for(i = 0; i < 3; i++)
    {
        acc_inc(ap);
    }
    printf("%d\n", acc_get(ap));
    acc_free(ap);
    return 0;
}

Various:

System:

All the above shows that Modula-2 is a very safe language. But for certain types of code (embedded, operating systems etc.) some more low level access to the system is necessary. And Modula-2 actually does offer such access.

First let us see that size of variables in memory and addresses of variables in memory are accessible via SYSTEM module.

MODULE SysEx;

FROM STextIO IMPORT WriteLn;
FROM SWholeIO IMPORT WriteInt, WriteCard;

FROM SYSTEM IMPORT INTEGER8, INTEGER16, INTEGER32, INTEGER64, CARDINAL8, CARDINAL16, CARDINAL32, CARDINAL64, WORD, BYTE, TSIZE, ADR, CAST;

VAR
    iv : INTEGER;
    cv : CARDINAL;
    xv : LONGREAL;
    sv : ARRAY [0..9] OF CHAR;
    i8 : INTEGER8;
    i16 : INTEGER16;
    i32 : INTEGER32;
    i64 : INTEGER64;
    c8 : CARDINAL8;
    c16 : CARDINAL16;
    c32 : CARDINAL32;
    c64 : CARDINAL64;
    wv : WORD;
    bv : BYTE;

BEGIN
    WriteCard(TSIZE(iv), 4);
    WriteInt(CAST(INTEGER, ADR(iv)), 12);
    WriteLn;
    WriteCard(TSIZE(cv), 4);
    WriteInt(CAST(INTEGER, ADR(cv)), 12);
    WriteLn;
    WriteCard(TSIZE(xv), 4);
    WriteInt(CAST(INTEGER, ADR(xv)), 12);
    WriteLn;
    WriteCard(TSIZE(sv), 4);
    WriteInt(CAST(INTEGER, ADR(sv)), 12);
    WriteLn;
    WriteCard(TSIZE(i8), 4);
    WriteInt(CAST(INTEGER, ADR(i8)), 12);
    WriteLn;
    WriteCard(TSIZE(i16), 4);
    WriteInt(CAST(INTEGER, ADR(i16)), 12);
    WriteLn;
    WriteCard(TSIZE(i32), 4);
    WriteInt(CAST(INTEGER, ADR(i32)), 12);
    WriteLn;
    WriteCard(TSIZE(i64), 4);
    WriteInt(CAST(INTEGER, ADR(i64)), 12);
    WriteLn;
    WriteCard(TSIZE(c8), 4);
    WriteInt(CAST(INTEGER, ADR(c8)), 12);
    WriteLn;
    WriteCard(TSIZE(c16), 4);
    WriteInt(CAST(INTEGER, ADR(c16)), 12);
    WriteLn;
    WriteCard(TSIZE(c32), 4);
    WriteInt(CAST(INTEGER, ADR(c32)), 12);
    WriteLn;
    WriteCard(TSIZE(c64), 4);
    WriteInt(CAST(INTEGER, ADR(c64)), 12);
    WriteLn;
    WriteCard(TSIZE(wv), 4);
    WriteInt(CAST(INTEGER, ADR(wv)), 12);
    WriteLn;
    WriteCard(TSIZE(bv), 4);
    WriteInt(CAST(INTEGER, ADR(bv)), 12);
    WriteLn;
END SysEx.

Note that the INTEGERn and CARDINALn types are not ISO standard but an extension supported by GNU Modula-2 and other implementations.

Coroutines:

Modula-2 also has support for cooperative/manual green threads called coroutines.

It is best explained with a simple example.

MODULE CoEx;

FROM STextIO IMPORT WriteString, WriteLn;

FROM SYSTEM IMPORT ADDRESS, ADR;
FROM COROUTINES IMPORT COROUTINE, NEWCOROUTINE, TRANSFER;

VAR
    retctx : ADDRESS;

PROCEDURE Run();

VAR
    dummy : ADDRESS;

BEGIN
    WriteString('B');
    WriteLn;
    TRANSFER(dummy, retctx);
    WriteString('D');
    WriteLn;
    TRANSFER(dummy, retctx);
END Run;

CONST
    wrksiz = 102400;

VAR
    runctx : COROUTINE;
    wrk : ARRAY [1..wrksiz] OF CHAR;

BEGIN
    NEWCOROUTINE(Run, ADR(wrk), wrksiz, runctx);
    WriteString('A');
    WriteLn;
    TRANSFER(retctx, runctx);
    WriteString('C');
    WriteLn;
    TRANSFER(retctx, runctx);
    WriteString('E');
    WriteLn;
END CoEx.

Output:

A
B
C
D
E

Note that this is the ISO version of Modula-2 - not the PIM version of Modula-2.

Integration with C/assembler:

Modula-2 can also call C/assembler code.

The specifics on how are compiler deppendent.

Here we will see how to do it with GNU Modula-2.

First the C code.

CLib.h:

int mdup(int n, const char *s, char *res, int *resactlen);

void va(int n, ...);

CLib.c:

#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#include "CLib.h"

int mdup(int n, const char *s, char *res, int *resactlen)
{
    int i;
    if(n < 1) return -1;
    strcpy(res, s);
    for(i = 1; i < n; i++) 
    {
        strcat(res, s);
    }
    *resactlen = 3 * n;
    return 0;
}

void va(int n, ...)
{
    va_list argptr;
    int i, v;
    va_start(argptr, n);
    for(i = 0; i < n; i++)
    {
        v = va_arg(argptr, int);
        printf(" %d", v);
    }
    printf("\n");
    va_end(argptr);
    return;
}

And then the Modula-2 code.

CLib.def:

DEFINITION MODULE FOR "C" CLib;

PROCEDURE mdup(n : CARDINAL; s : ARRAY OF CHAR; VAR res : ARRAY OF CHAR; VAR reslen : CARDINAL) : INTEGER;

PROCEDURE va(n : INTEGER; ...);

END CLib.

The FOR "C" is very important as that changes calling from Modula-2 style to C style avoiding having the C code comply with Modula-2 style.

M2C.mod:

MODULE M2C;

FROM Strings IMPORT Length;
FROM STextIO IMPORT WriteString, WriteLn;
FROM SWholeIO IMPORT WriteInt, WriteCard;

FROM CLib IMPORT mdup, va;

VAR
    demo : ARRAY [0..99] OF CHAR;
    demolen : CARDINAL;
    res : INTEGER;

BEGIN
    res := mdup(3, 'ABC', demo, demolen);
    WriteInt(res, 2);
    WriteLn;
    WriteString(demo);
    WriteLn;
    WriteCard(demolen, 1);
    WriteLn;
    WriteCard(Length(demo), 1);
    WriteLn;
    va(0);
    va(1, 1);
    va(2, 1, 2);
    va(3, 1, 2, 3);
END M2C.

Build and run:

gcc -c CLib.c
gm2 -fiso M2C.mod CLib.o -o M2C
./M2C

Output:

+0
ABCABCABC
9
9
 1
 1 2
 1 2 3

And now an example with inline assembler:

MODULE M2A;

FROM STextIO IMPORT WriteLn;
FROM SWholeIO IMPORT WriteInt;

PROCEDURE add(a, b : INTEGER) : INTEGER;

VAR
    res : INTEGER;
    
BEGIN
    ASM VOLATILE('movl %1,%%eax; addl %2,%%eax; movl %%eax,%0' : '=g' (res) : 'g' (a), 'g' (b));
    RETURN(res);
END add;

BEGIN
    WriteInt(add(123, 456), 4);
    WriteLn;
END M2A.

Output:

+579

The assembler syntax is AT&T assembler syntax and the ASM works like asm in other GNU compilers.

Article history:

Version Date Description
1.0 August 27th 2021 Initial version
1.1 August 28th 2021 Add system/coroutines/C interface section
1.2 September 2nd 2021 Update C interface section and add assembler example

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj