This tutorial walks you through using the V2 compiler: building it,
compiling small Modula-2 programs, running them on the MC64 interpreter
(mcint), and understanding the output. Every example below was actually
compiled and run to produce the shown result.
You don't need any tooling besides the project itself, GNU Modula-2 (gm2),
the Coco/R generator (CR), and the mcint interpreter.
The V2 language has no text I/O statements (that is V1's
WriteString/WriteInt), so a program signals its result by setting a global variableVAR ExitCode : INTEGER;. If present, the generated program prints its value as a decimal number (plus a newline) and then exits.
Two prerequisites live as sibling projects of this one:
| what | where | environment override |
|---|---|---|
| Coco/R generator | ../CocoGm2/CR |
CRBIN |
| MC64 interpreter | ../m-code-64/mcint |
MCINT |
Everything here assumes you are in the project root:
cd .../m2compiler-V2
./build.sh # regenerate Coco/R sources, compile with gm2 -fiso, link ./M2comp
./build.sh runs Coco/R on src/M2comp.atg (the grammar reports
"Compilation completed. No errors detected."), recompiles the hand-written
modules (FileIO, SymTab, AST, MGen), and links the compiler binary
./M2comp.
From here on we use mcint with the default sibling path:
MCINT=../m-code-64/mcint
Save a program in a .mod file and run the compiler on it:
./M2comp hello.mod
The compiler prints to standard output, for every unit of the session:
Parsing hello.mod
--- Symbol table ---
INTEGER : PREDEF #0
...
Hello : MODULE #-1
ExitCode : VAR #0
--- AST ---
Module Hello
Var ExitCode :0
Block
Assign
Name ExitCode :0
Int 42 :0
...
Parsing ..., the symbol table dump and the --- AST --- tree
(a teaching feature of this compiler: V2 builds a full syntax tree before
emitting code).<file>.LST is written with the
source text and any errors marked with a ^ and a message.--- Code <Module> --- (global layout, procedure table, disassembled
image) and then the verdict: Parsed correctly or Incorrect
source.<Module>.MC4 — named from the MODULE name, not
the file name — is written into the current directory.Run the image on the MC64 interpreter:
$MCINT Hello.MC4 # right after a successful ./M2comp hello.mod
MODULE Hello;
VAR ExitCode : INTEGER;
BEGIN
ExitCode := 42
END Hello.
./M2comp hello.mod # -> "Parsed correctly", writes Hello.MC4
$MCINT Hello.MC4
Output:
42
The embedded helper prints ExitCode = 42 followed by a line break.
All structured statements: IF/ELSIF/ELSE, WHILE, REPEAT,
LOOP/EXIT, and FOR (including a BY step and a downward loop).
MODULE Control;
(* Structured statements: IF/ELSIF/ELSE, WHILE, REPEAT,
LOOP with EXIT, and FOR (with BY, and downward). *)
VAR ExitCode : INTEGER;
s, i : INTEGER;
BEGIN
s := 0;
IF 1 > 2 THEN s := 1
ELSIF 2 > 3 THEN s := 2
ELSE s := 3
END;
i := 0;
WHILE i < 10 DO i := i + 1; s := s + i END;
REPEAT s := s - 1 UNTIL s < 60;
LOOP
s := s + 1;
IF s >= 60 THEN EXIT END
END;
FOR i := 1 TO 5 BY 2 DO s := s + i END;
FOR i := 10 TO 1 DO s := s + 0 END;
ExitCode := s
END Control.
./M2comp control.mod
$MCINT Control.MC4
Output:
69
Nested procedures, value and VAR parameters, recursive functions with
RETURN.
MODULE Procs;
(* Recursive functions, VALUE and VAR parameters, nested procedures. *)
VAR ExitCode : INTEGER;
PROCEDURE Fact(n : INTEGER) : INTEGER;
BEGIN
IF n <= 1 THEN RETURN 1 END;
RETURN n * Fact(n - 1)
END Fact;
PROCEDURE Bump(VAR x : INTEGER); (* VAR parameter: changes caller *)
BEGIN
x := x + 1
END Bump;
PROCEDURE Sum2(a, b : INTEGER) : INTEGER;
VAR loc : INTEGER;
PROCEDURE Double(t : INTEGER) : INTEGER; (* nested procedure *)
BEGIN
RETURN t * 2
END Double;
BEGIN
loc := a + b;
RETURN loc + Double(loc)
END Sum2;
BEGIN
ExitCode := Fact(5); (* 120 *)
Bump(ExitCode); (* 121 *)
ExitCode := ExitCode + Sum2(10, 5) (* 121 + 45 *)
END Procs.
./M2comp procs.mod
$MCINT Procs.MC4
Output:
166
MODULE Records;
(* RECORD fields, WITH abbreviation, whole-record copy. *)
VAR ExitCode : INTEGER;
r, q : RECORD x, y : INTEGER END;
BEGIN
r.x := 10;
r.y := 20;
WITH r DO (* fields of r in scope *)
x := x + 1; (* r.x := 11 *)
y := y + x (* r.y := 31 *)
END;
q := r; (* whole-record copy *)
ExitCode := q.x + q.y
END Records.
./M2comp records.mod
$MCINT Records.MC4
Output:
42
WITH nests (including on array elements, e.g. WITH a[1] DO ... END), and
whole records copy with q := r.
CASE works on ordinal and CHAR selectors, with comma-separated label
lists, .. ranges, and ELSE.
MODULE CaseDemo;
(* CASE over integer and CHAR selectors: label lists, ranges, ELSE. *)
VAR ExitCode : INTEGER;
i, k : INTEGER;
c : CHAR;
BEGIN
ExitCode := 0;
i := 2;
CASE i OF
1: ExitCode := ExitCode + 1
| 2, 3: ExitCode := ExitCode + 10
| 4..6: ExitCode := ExitCode + 100
ELSE ExitCode := ExitCode + 1000
END;
k := 7;
CASE k OF
1: k := 0
ELSE k := k + 1
END;
ExitCode := ExitCode + k;
c := 'b';
CASE c OF
'a': ExitCode := ExitCode + 100
| 'b': ExitCode := ExitCode + 20
ELSE ExitCode := ExitCode + 200
END;
i := 99;
CASE i OF
1: i := 0
ELSE i := i + 1
END;
ExitCode := ExitCode + i
END CaseDemo.
./M2comp casedemo.mod
$MCINT CaseDemo.MC4
Output:
138
Strings are ARRAY [lo .. hi] OF CHAR; you assign a literal, copy the whole
array, and index elements (each index is a CHAR).
MODULE Strings;
(* Strings are ARRAY ... OF CHAR; literals index as CHAR. *)
VAR ExitCode : INTEGER;
s, t : ARRAY [0 .. 7] OF CHAR;
BEGIN
ExitCode := 0;
s := "hi";
t := s; (* whole-array copy *)
IF s[0] = "h" THEN ExitCode := ExitCode + 1 END;
IF s[1] = "i" THEN ExitCode := ExitCode + 10 END;
IF t[0] = "h" THEN ExitCode := ExitCode + 100 END;
s[0] := 'H'; (* single char assignment *)
s[1] := '!';
IF s[1] = "!" THEN ExitCode := ExitCode + 1000 END
END Strings.
./M2comp strings.mod
$MCINT Strings.MC4
Output:
1111
Gotcha: a single-character literal ("h", 'b') is a CHAR, not a
string — exactly right for s[0] = "h".
Fixed arrays, multi-dimensional arrays, nested loops, and whole-array copy.
MODULE Arrays;
(* One- and two-dimensional arrays, whole-array copy. *)
VAR ExitCode : INTEGER;
v, w : ARRAY [0 .. 9] OF INTEGER;
m : ARRAY [0 .. 2], [0 .. 3] OF INTEGER;
i, j : INTEGER;
BEGIN
FOR i := 0 TO 9 DO v[i] := i * 2 END;
w := v; (* whole-array copy *)
FOR i := 0 TO 2 DO
FOR j := 0 TO 3 DO m[i, j] := i * 10 + j END
END;
ExitCode := v[5] + w[3] + m[2, 3]
END Arrays.
./M2comp arrays.mod
$MCINT Arrays.MC4
Output:
39
REAL (binary64) arithmetic with decimal and E-notation literals, and
comparisons. Note the unary minus on a real expression.
MODULE Reals;
(* REAL arithmetic: + - * / , E-notation, comparisons. *)
VAR ExitCode : INTEGER;
r : REAL;
BEGIN
ExitCode := 0;
r := 1.5 + 2.5;
IF r = 4.0 THEN ExitCode := ExitCode + 1 END;
r := 10.0 / 4.0;
IF (r > 2.4) AND (r < 2.6) THEN ExitCode := ExitCode + 10 END;
r := 3.5 - 1.5 * 2.0;
IF r = 0.5 THEN ExitCode := ExitCode + 100 END;
IF -r < -0.25 THEN ExitCode := ExitCode + 1000 END
END Reals.
./M2comp reals.mod
$MCINT Reals.MC4
Output:
1111
INTEGER values widen to REAL automatically in mixed expressions.
A local MODULE M (Wirth form) with an EXPORT list hides its locals and
exposes M.x / M.Proc() to the enclosing program. Optional module BEGIN
init bodies run at startup, before the program's own statements, in
declaration order.
MODULE LocalMod;
(* Local module (Wirth form) with EXPORT list and an init body. *)
VAR ExitCode : INTEGER;
MODULE M;
EXPORT q, Get;
VAR q : INTEGER;
PROCEDURE Get() : INTEGER;
BEGIN
RETURN q + 1
END Get;
BEGIN
q := 41 (* module init runs before program body *)
END M;
BEGIN
ExitCode := M.q + M.Get() (* 41 + 42 *)
END LocalMod.
./M2comp localmod.mod
$MCINT LocalMod.MC4
Output:
83
MODULE M [n]; local forms with a priority number are accepted as well.
SET OF <ordinal type>: assignment, whole-set copy, IN, = and #.
(V2 has no set literals {...} yet — sets start empty and you test them.)
MODULE Sets;
(* SET OF an ordinal base: assignment, IN, equality/inequality. *)
TYPE Sub = [0 .. 9];
VAR ExitCode : INTEGER;
s, t : SET OF Sub;
b : BOOLEAN;
BEGIN
ExitCode := 0;
t := s; (* set copy (both empty) *)
b := 3 IN s; (* FALSE: s is empty *)
IF b THEN ExitCode := 1 ELSE ExitCode := 2 END;
IF s = t THEN ExitCode := ExitCode + 10 END;
IF s # t THEN ExitCode := ExitCode + 100 END
END Sets.
./M2comp sets.mod
$MCINT Sets.MC4
Output:
12
V2 compiles a library split across a definition and an implementation, plus a
client — all in one session — into a single image. Session order is
definitions, then implementations, then exactly one program module; the
program must come last. The library's BEGIN init body runs before the
program body.
mathlib.def:
DEFINITION MODULE MathLib;
CONST PiSq = 9;
VAR calls : INTEGER;
PROCEDURE Square(a : INTEGER) : INTEGER;
PROCEDURE Bump(VAR x : INTEGER);
END MathLib.
mathlib.mod (the implementation):
IMPLEMENTATION MODULE MathLib;
VAR total : INTEGER;
PROCEDURE Square(a : INTEGER) : INTEGER;
BEGIN
RETURN a * a
END Square;
PROCEDURE Bump(VAR x : INTEGER);
BEGIN
x := x + 1
END Bump;
BEGIN
total := 0; (* module init: runs before the program body *)
calls := 0
END MathLib.
app.mod:
MODULE App;
FROM MathLib IMPORT Square;
IMPORT MathLib;
VAR ExitCode : INTEGER;
n : INTEGER;
BEGIN
n := Square(3) + MathLib.PiSq; (* 9 + 9 = 18 *)
MathLib.Bump(n); (* 19 *)
ExitCode := n + MathLib.calls (* 19 + 0 *)
END App.
Compile the three files on one command line:
./M2comp mathlib.def mathlib.mod app.mod
$MCINT App.MC4
Output:
19
Imports resolve against completed definitions: FROM MathLib IMPORT Square
(unqualified call), MathLib.PiSq (constant), MathLib.Bump (qualified
procedure). An implementation heading that doesn't match its definition is
error 231; importing an unknown or unimplemented module is 201.
Say you assign an INTEGER to a BOOLEAN:
MODULE BadTest;
VAR i : INTEGER;
b : BOOLEAN;
ExitCode : INTEGER;
BEGIN
i := 1;
b := i; (* wrong: BOOLEAN := INTEGER *)
ExitCode := i
END BadTest.
./M2comp badtest.mod
stdout ends with:
Incorrect source
and badtest.LST shows the offending line with a caret and message:
Listing:
1 MODULE BadTest;
2 VAR i : INTEGER;
3 b : BOOLEAN;
4 ExitCode : INTEGER;
5 BEGIN
6 i := 1;
7 b := i; (* wrong: BOOLEAN := INTEGER *)
***** ^ incompatible assignment
8 ExitCode := i
9 END BadTest.
1 error
Static semantic errors use numeric codes 200–233 (201 undeclared
identifier, 210 incompatible assignment, 231 forward mismatch, 233
invalid procedure call, …); constructs the compiler does not lower yet are
rejected with 230 ("not supported in this phase"). See the table in
README.md.
For scripting:
M2compreturns exit status 0 even when the source is rejected — judge success by theParsed correctly/Incorrect sourceverdict on stdout, or by the absence of a generated.MC4file.
pi_sq does
not lex; use PiSq.ExitCode
convention described above.CHAR, not a string.INTEGER DIV/MOD truncate toward zero; AND/OR are eager (no
short-circuit); # and <> are the same operator.x in a
second heading of the same DEFINITION (duplicate identifier, error 200).(A, B, C)), set literals
{...}, NEW/DISPOSE, and standard procedures (HIGH, INC, DEC);
open arrays exist only as VAR formals. Opaque types are the planned
next step.NIL dereference reads 0 —
no runtime trap yet.tests/showcase.mod and tests/showcase5.mod — two "tours" exercising the
whole language; docs/summary_m2comp_step*.md — one development-step
summary each (what was added, test counts, bugs found).docs/goal_step8.md — the roadmap (opaque types, procedure types, quad
backend).README.md — full feature list, error-code table, layout and build
details.tests/ — the 65 programs of the regression suite used by run_tests.sh;
a great source of runnable examples for every construct.