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.
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:
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.
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.
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.
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:
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.
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.
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.
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.
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.
Modula-2 enums are called scalars.
TYPE
typename = (val1, val2, ...);
...
VAR
varname : typename;
Modula-2 like Pascal and Ada (but unlike C/C++/Java/C#) support subranges of integers.
TYPE
typename = [startval..endval];
...
VAR
varname : typename;
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
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.
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
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
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;
}
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.
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.
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.
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 |
See list of all articles here
Please send comments to Arne Vajhøj