tutorial.md 14 KB

M2comp tutorial — compiling Modula-2 to the MC64 virtual machine

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 variable VAR ExitCode : INTEGER;. If present, the generated program prints its value as a decimal number (plus a newline) and then exits.


1. Setup

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

2. The compile → run cycle

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).
  • Alongside each source file, a listing <file>.LST is written with the source text and any errors marked with a ^ and a message.
  • After a successful session the compiler prints the step-7 backend listing --- Code <Module> --- (global layout, procedure table, disassembled image) and then the verdict: Parsed correctly or Incorrect source.
  • The bytecode image <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

3. Example 1 — Hello, MC64!

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.


4. Example 2 — Control flow

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

5. Example 3 — Procedures and functions

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

6. Example 4 — Records and WITH

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.


7. Example 5 — CASE

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

8. Example 6 — Strings

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".


9. Example 7 — Arrays

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

10. Example 8 — REAL arithmetic

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.


11. Example 9 — Local modules

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.


12. Example 10 — Sets

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

13. Example 11 — Separate compilation units

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.


14. Example 12 — When things go wrong

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: M2comp returns exit status 0 even when the source is rejected — judge success by the Parsed correctly / Incorrect source verdict on stdout, or by the absence of a generated .MC4 file.


15. Language gotchas and limits

  • Identifiers are only letters and digits — no underscores. pi_sq does not lex; use PiSq.
  • No text I/O statements: the only output channel is the ExitCode convention described above.
  • A single-character literal is a CHAR, not a string.
  • INTEGER DIV/MOD truncate toward zero; AND/OR are eager (no short-circuit); # and <> are the same operator.
  • Definitions scope their formal parameter names: don't reuse x in a second heading of the same DEFINITION (duplicate identifier, error 200).
  • Not in this generation yet: enum types ((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.
  • Array indexing is not range-checked, and NIL dereference reads 0 — no runtime trap yet.
  • Current caps: 256 symbols, 8-deep call stack, 7 array dimensions, 15 library units per session.

16. Where to go next

  • 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.