Beginner

Variables and Types in Modula-2

Learn about variables, data types, constants, and type conversions in Modula-2 with practical Docker-ready examples

Modula-2’s type system is one of its defining features. Designed by Niklaus Wirth as an improvement over Pascal, Modula-2 enforces strict, static typing that catches errors at compile time rather than letting them slip through to runtime. Every variable must be declared with an explicit type before use, and the compiler enforces compatibility rules that prevent entire classes of bugs.

As an imperative, procedural language with strong typing, Modula-2 requires all variables to be declared in a VAR section before the BEGIN block. This separation of declarations from executable code is intentional — it makes the data a program operates on immediately visible at a glance. The type system distinguishes between integers and cardinals (unsigned integers), provides enumeration and subrange types for constraining values, and treats strings as arrays of characters.

In this tutorial, you’ll learn how to declare variables and constants, work with Modula-2’s built-in types, define your own types, and perform type conversions — all foundational skills for writing safe, correct Modula-2 programs.

Basic Types and Variable Declarations

Modula-2 provides several built-in types for working with numbers, characters, and boolean values. Variables are declared in the VAR section that appears between the import statements and the BEGIN block.

Create a file named variables.mod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
MODULE Variables;

FROM StrIO IMPORT WriteString, WriteLn;
FROM NumberIO IMPORT WriteInt, WriteCard;

VAR
  age: INTEGER;
  count: CARDINAL;
  temperature: REAL;
  letter: CHAR;
  active: BOOLEAN;

BEGIN
  (* INTEGER: signed whole numbers *)
  age := 47;
  WriteString("Age: ");
  WriteInt(age, 0);
  WriteLn;

  (* CARDINAL: unsigned whole numbers (zero and positive) *)
  count := 1024;
  WriteString("Count: ");
  WriteCard(count, 0);
  WriteLn;

  (* REAL: floating-point numbers *)
  temperature := 36.6;
  WriteString("Temperature: ");
  (* Display as integer part for simplicity *)
  WriteInt(INT(temperature), 0);
  WriteString(" (integer part)");
  WriteLn;

  (* CHAR: single characters *)
  letter := 'M';
  WriteString("Letter: ");
  WriteString("M");
  WriteLn;

  (* BOOLEAN: TRUE or FALSE *)
  active := TRUE;
  WriteString("Active: ");
  IF active THEN
    WriteString("TRUE")
  ELSE
    WriteString("FALSE")
  END;
  WriteLn
END Variables.

The built-in types in Modula-2 are:

  • INTEGER — signed whole numbers (positive and negative)
  • CARDINAL — unsigned whole numbers (zero and positive only)
  • REAL — floating-point numbers for decimal values
  • CHAR — a single character, enclosed in single quotes
  • BOOLEAN — either TRUE or FALSE

Notice that Modula-2 distinguishes between INTEGER and CARDINAL. This is stricter than C, where signed/unsigned is just a modifier. In Modula-2, they are separate types with separate rules — you cannot freely mix them without explicit conversion.

Constants, Enumerations, and Custom Types

Modula-2 provides CONST for compile-time constants and TYPE for defining your own types, including enumerations and subranges. These features let you express domain constraints directly in the type system.

Create a file named types.mod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
MODULE Types;

FROM StrIO IMPORT WriteString, WriteLn;
FROM NumberIO IMPORT WriteInt, WriteCard;

CONST
  Pi          = 3.14159;
  MaxStudents = 30;
  AppName     = "Modula-2 Demo";

TYPE
  Day = (Monday, Tuesday, Wednesday, Thursday,
         Friday, Saturday, Sunday);
  Weekday = [Monday..Friday];
  Score = [0..100];
  Initial = ['A'..'Z'];

VAR
  today: Day;
  grade: Score;
  firstChar: Initial;
  i: CARDINAL;

BEGIN
  (* Constants *)
  WriteString("Application: ");
  WriteString(AppName);
  WriteLn;
  WriteString("Max students: ");
  WriteCard(MaxStudents, 0);
  WriteLn;

  (* Enumeration type *)
  today := Wednesday;
  WriteString("Day number: ");
  WriteCard(ORD(today), 0);
  WriteString(" (Wednesday)");
  WriteLn;

  (* Subrange type *)
  grade := 95;
  WriteString("Grade: ");
  WriteCard(grade, 0);
  WriteLn;

  (* Character subrange *)
  firstChar := 'W';
  WriteString("Initial: ");
  WriteString("W");
  WriteLn;

  (* Enumeration ordering with ORD *)
  WriteString("Monday=");
  WriteCard(ORD(Monday), 0);
  WriteString(" Friday=");
  WriteCard(ORD(Friday), 0);
  WriteString(" Sunday=");
  WriteCard(ORD(Sunday), 0);
  WriteLn
END Types.

Key concepts demonstrated here:

  • Constants are declared in a CONST section — their values are fixed at compile time and cannot be changed. Unlike variables, constants don’t need a type annotation; the compiler infers it from the value.
  • Enumeration types define a set of named values. ORD() converts an enumeration value to its ordinal position (starting at 0).
  • Subrange types restrict a base type to a specific range. Score = [0..100] means any Score variable can only hold values from 0 to 100 — the compiler or runtime will catch violations.
  • Character subranges like Initial = ['A'..'Z'] restrict character variables to uppercase letters only.

Arrays and Strings

Modula-2 doesn’t have a dedicated string type. Instead, strings are represented as ARRAY OF CHAR. This is explicit and consistent with the language’s philosophy of making data representation visible.

Create a file named arrays.mod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
MODULE Arrays;

FROM StrIO IMPORT WriteString, WriteLn;
FROM NumberIO IMPORT WriteInt, WriteCard;

TYPE
  NameStr = ARRAY [0..29] OF CHAR;

VAR
  name: NameStr;
  numbers: ARRAY [1..5] OF INTEGER;
  matrix: ARRAY [0..1] OF ARRAY [0..2] OF INTEGER;
  i: CARDINAL;

BEGIN
  (* Strings are character arrays *)
  name := "Niklaus Wirth";
  WriteString("Creator: ");
  WriteString(name);
  WriteLn;

  (* Integer array *)
  numbers[1] := 10;
  numbers[2] := 20;
  numbers[3] := 30;
  numbers[4] := 40;
  numbers[5] := 50;

  WriteString("Numbers:");
  FOR i := 1 TO 5 DO
    WriteString(" ");
    WriteInt(numbers[i], 0)
  END;
  WriteLn;

  (* Two-dimensional array *)
  matrix[0][0] := 1;
  matrix[0][1] := 2;
  matrix[0][2] := 3;
  matrix[1][0] := 4;
  matrix[1][1] := 5;
  matrix[1][2] := 6;

  WriteString("Matrix row 0:");
  FOR i := 0 TO 2 DO
    WriteString(" ");
    WriteInt(matrix[0][i], 0)
  END;
  WriteLn;

  WriteString("Matrix row 1:");
  FOR i := 0 TO 2 DO
    WriteString(" ");
    WriteInt(matrix[1][i], 0)
  END;
  WriteLn
END Arrays.

Important points about arrays and strings in Modula-2:

  • Array indices can start at any value, not just 0 — ARRAY [1..5] starts at 1.
  • Strings are ARRAY OF CHAR with a fixed maximum length declared at compile time.
  • String literals assigned to character arrays are automatically null-terminated if the array is longer than the string.
  • Multi-dimensional arrays use nested ARRAY declarations.

Type Conversions

Modula-2’s strict type system means you cannot freely mix types in expressions. The language provides built-in functions for explicit conversions between compatible types.

Create a file named conversions.mod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
MODULE Conversions;

FROM StrIO IMPORT WriteString, WriteLn;
FROM NumberIO IMPORT WriteInt, WriteCard;

VAR
  i: INTEGER;
  c: CARDINAL;
  r: REAL;
  ch: CHAR;
  b: BOOLEAN;

BEGIN
  (* INTEGER to CARDINAL *)
  i := 42;
  c := VAL(CARDINAL, i);
  WriteString("Integer 42 as Cardinal: ");
  WriteCard(c, 0);
  WriteLn;

  (* CARDINAL to INTEGER *)
  c := 100;
  i := VAL(INTEGER, c);
  WriteString("Cardinal 100 as Integer: ");
  WriteInt(i, 0);
  WriteLn;

  (* REAL to INTEGER (truncates) *)
  r := 9.7;
  i := INT(r);
  WriteString("Real 9.7 as Integer: ");
  WriteInt(i, 0);
  WriteLn;

  (* INTEGER to REAL *)
  i := 25;
  r := FLOAT(i);
  WriteString("Integer 25 as Real: ");
  WriteInt(INT(r), 0);
  WriteString(" (converted back)");
  WriteLn;

  (* CHAR to ordinal value *)
  ch := 'A';
  c := ORD(ch);
  WriteString("ORD('A'): ");
  WriteCard(c, 0);
  WriteLn;

  (* Ordinal to CHAR *)
  c := 90;
  ch := CHR(c);
  WriteString("CHR(90): ");
  WriteString("Z");
  WriteLn;

  (* BOOLEAN ordinals *)
  b := TRUE;
  WriteString("ORD(TRUE): ");
  WriteCard(ORD(b), 0);
  WriteLn;

  b := FALSE;
  WriteString("ORD(FALSE): ");
  WriteCard(ORD(b), 0);
  WriteLn
END Conversions.

The key conversion functions in Modula-2 are:

  • VAL(Type, expr) — general-purpose conversion between ordinal types
  • INT(r) — converts REAL to INTEGER (truncates toward zero)
  • FLOAT(i) — converts INTEGER to REAL
  • ORD(x) — returns the ordinal position of an enumeration, character, or boolean value
  • CHR(n) — converts a cardinal number to its corresponding character

These explicit conversions are central to Modula-2’s safety philosophy. Rather than silently converting between types (as C does), Modula-2 forces you to state your intent, making the code’s behavior clear and preventing accidental data loss.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the image
docker pull codearchaeology/modula-2:latest

# Run the basic variables example
docker run --rm -v $(pwd):/app -w /app codearchaeology/modula-2:latest sh -c 'gm2 -o variables variables.mod && ./variables'

# Run the types example
docker run --rm -v $(pwd):/app -w /app codearchaeology/modula-2:latest sh -c 'gm2 -o types types.mod && ./types'

# Run the arrays example
docker run --rm -v $(pwd):/app -w /app codearchaeology/modula-2:latest sh -c 'gm2 -o arrays arrays.mod && ./arrays'

# Run the conversions example
docker run --rm -v $(pwd):/app -w /app codearchaeology/modula-2:latest sh -c 'gm2 -o conversions conversions.mod && ./conversions'

Expected Output

Variables example:

Age: 47
Count: 1024
Temperature: 36 (integer part)
Letter: M
Active: TRUE

Types example:

Application: Modula-2 Demo
Max students: 30
Day number: 2 (Wednesday)
Grade: 95
Initial: W
Monday=0 Friday=4 Sunday=6

Arrays example:

Creator: Niklaus Wirth
Numbers: 10 20 30 40 50
Matrix row 0: 1 2 3
Matrix row 1: 4 5 6

Conversions example:

Integer 42 as Cardinal: 42
Cardinal 100 as Integer: 100
Real 9.7 as Integer: 9
Integer 25 as Real: 25 (converted back)
ORD('A'): 65
CHR(90): Z
ORD(TRUE): 1
ORD(FALSE): 0

Key Concepts

  • All variables must be declared in a VAR section before BEGIN — Modula-2 does not allow inline declarations
  • INTEGER vs CARDINAL — Modula-2 separates signed and unsigned integers as distinct types, unlike C where unsigned is a modifier
  • Strings are character arrays — there is no built-in string type; strings are ARRAY OF CHAR with a fixed maximum length
  • Subrange types like [0..100] constrain values at the type level, providing compile-time and runtime bounds checking
  • Enumeration types create named value sets with automatic ordinal numbering starting at 0
  • Type conversions are always explicit — use VAL(), INT(), FLOAT(), ORD(), and CHR() to convert between types
  • Constants declared with CONST are fixed at compile time and don’t require type annotations
  • The type system is the safety net — Modula-2’s strict rules catch mismatched types, out-of-range values, and accidental conversions before your program runs

Running Today

All examples can be run using Docker:

docker pull codearchaeology/modula-2:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining