tutorial.md 12 KB

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

This tutorial walks you through using the V1 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 language has no text I/O library, so programs signal their 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. The built-in statements WriteString("...") and WriteInt(42) write text directly.


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-V1

./build.sh      # regenerate Coco/R sources, compile with gm2 -fiso, link ./M2c

./build.sh runs Coco/R on src/M2c.atg to regenerate the scanner, parser and driver (src/M2cS.mod, src/M2cP.mod, src/M2c.mod), compiles the hand-written modules (FileIO, SymTab, MGen), and links the compiler binary ./M2c.

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:

./M2c hello.mod

The compiler prints to standard output:

Parsing hello.mod

--- Symbol table ---
  INTEGER : PREDEF #0
  ...
  Hello : MODULE #-1
  ExitCode : VAR #0
Parsed correctly
  • Parsing ... then the symbol table dump (a teaching feature of this compiler), then the verdict: Parsed correctly or Incorrect source.
  • Alongside each source file, a listing <file>.LST is written with the source text and any errors marked with a ^ and a message.
  • On success the compiler writes a bytecode image <Module>.MC4 — named from the MODULE name, not the file name — into the current directory.

Run the image on the MC64 interpreter:

$MCINT Hello.MC4    # right after a successful ./M2c hello.mod

3. Example 1 — Hello, MC64!

MODULE Hello;
VAR ExitCode : INTEGER;
BEGIN
  WriteString("Hello, MC64!");
  ExitCode := 42
END Hello.
./M2c hello.mod     # -> "Parsed correctly", writes Hello.MC4
$MCINT Hello.MC4

Output:

Hello, MC64!42

WriteString prints the text without a newline, and the embedded helper then prints ExitCode = 42 followed by a line break.


4. Example 2 — Control flow

All structured statements: FOR, WHILE, REPEAT, LOOP/EXIT, CASE, IF/ELSIF/ELSE.

MODULE Control;
CONST Limit = 20;
VAR i, sum, n : INTEGER;
VAR ExitCode : INTEGER;
BEGIN
  sum := 0;
  FOR i := 1 TO 10 DO sum := sum + i END;   (* sum = 55 *)
  n := sum MOD Limit;                        (* n = 15 *)
  WHILE n < 0 DO n := n + 1 END;
  REPEAT n := n + 1 UNTIL n >= 17;           (* n = 17 *)
  LOOP
    IF n > 100 THEN EXIT END;
    n := n + 1;
    IF n >= 19 THEN EXIT END
  END;                                       (* n = 19 *)
  CASE n OF
    19 : ExitCode := sum + n
  ELSE ExitCode := 0
  END
END Control.
./M2c control.mod
$MCINT Control.MC4

Output:

74

5. Example 3 — Procedures, functions and open arrays

Nested procedures, value and VAR parameters, functions with RETURN (including recursion), and open-array formals with HIGH.

MODULE Procs;
VAR a : ARRAY [1 .. 5] OF INTEGER;
VAR acc : INTEGER;
VAR ExitCode : INTEGER;

PROCEDURE Fact(n : INTEGER) : INTEGER;      (* recursive function *)
BEGIN
  IF n <= 1 THEN RETURN 1 ELSE RETURN n * Fact(n - 1) END
END Fact;

PROCEDURE Accumulate(VAR total : INTEGER; x : INTEGER);  (* VAR param *)
BEGIN
  total := total + x
END Accumulate;

PROCEDURE SumOpen(x : ARRAY OF INTEGER) : INTEGER;   (* open array *)
VAR i, s : INTEGER;
BEGIN
  s := 0;
  FOR i := 0 TO HIGH(x) DO s := s + x[i] END;
  RETURN s
END SumOpen;

BEGIN
  a[1] := 1; a[2] := 2; a[3] := 3; a[4] := 4; a[5] := 5;
  acc := 0;
  Accumulate(acc, Fact(5));        (* acc := 120 *)
  Accumulate(acc, SumOpen(a));     (* acc := 120 + 15 *)
  ExitCode := acc
END Procs.
./M2c procs.mod
$MCINT Procs.MC4

Output:

135

6. Example 4 — Records and WITH

MODULE Records;
TYPE
  Point  = RECORD x, y : INTEGER END;
  Person = RECORD name : ARRAY [1 .. 10] OF CHAR; age : INTEGER END;
VAR p : Point;
VAR me : Person;
VAR ExitCode : INTEGER;
BEGIN
  p.x := 3;
  p.y := 4;
  me.name := "Ada";
  me.age := 36;
  WITH me DO                           (* fields of me in scope *)
    age := age + 1                     (* same as me.age := me.age + 1 *)
  END;
  ExitCode := p.x + p.y + me.age       (* 3 + 4 + 37 *)
END Records.
./M2c records.mod
$MCINT Records.MC4

Output:

44

Records nest, fields can be composite (rec.b.u), and whole records copy with q := p.


7. Example 5 — Pointers: a linked list

POINTER TO, NEW, ^ dereference and NIL. Indicative DISPOSE exists too (it maps to the VM DEALLOCATE). Note that NIL dereference currently reads as 0 and arrays are not range-checked — see the limits section.

MODULE List;
TYPE Node = RECORD val : INTEGER; next : POINTER TO Node END;
VAR head, q : POINTER TO Node;
VAR total : INTEGER;
VAR ExitCode : INTEGER;

PROCEDURE Push(v : INTEGER);
VAR n : POINTER TO Node;
BEGIN
  NEW(n);
  n^.val := v;
  n^.next := head;
  head := n
END Push;

BEGIN
  head := NIL;
  Push(1); Push(2); Push(3); Push(4);      (* list is 4 -> 3 -> 2 -> 1 *)
  total := 0;
  q := head;
  WHILE q # NIL DO
    total := total + q^.val;               (* 1 + 2 + 3 + 4 *)
    q := q^.next
  END;
  ExitCode := total
END List.
./M2c list.mod
$MCINT List.MC4

Output:

10

8. Example 6 — Strings

Strings are ARRAY [1 .. n] OF CHAR; you assign a literal, compare with =/#/</> and index elements.

MODULE Strings;
VAR s, t : ARRAY [1 .. 10] OF CHAR;
VAR ExitCode : INTEGER;
BEGIN
  s := "hello";
  t := "hello";
  IF s = t THEN ExitCode := 1 ELSE ExitCode := 0 END;   (* 1 *)
  t := "world";
  IF s # t THEN ExitCode := ExitCode + 2 END;            (* +2 *)
  IF s < t THEN ExitCode := ExitCode + 4 END;            (* +4 *)
  WriteString(s)
END Strings.
./M2c strings.mod
$MCINT Strings.MC4

Output:

hello7

(The 7 is the printed ExitCode: 1 + 2 + 4.)

Gotcha: a single-character literal such as " " is treated as a CHAR, not a string — a WriteString(" ") call does not print that character. Use multi-character literals for string data.


9. Example 7 — Sets and enumerations

SET OF with literal elements, .. ranges, IN, = and the +/-/* operators; enumerations with a CASE over the literals.

MODULE Sets;
TYPE Color = (Red, Green, Blue);
VAR s, t : SET OF [0 .. 7];
VAR c : Color;
VAR ExitCode : INTEGER;
BEGIN
  s := {1, 2, 3};
  t := {5 .. 7};
  c := Green;
  ExitCode := 0;
  IF (2 IN s) AND (s = {1, 2, 3}) THEN ExitCode := 14 ELSE ExitCode := 0 END;
  IF 6 IN t THEN ExitCode := ExitCode + 1 END;
  CASE c OF
    Red   : ExitCode := ExitCode + 0 |
    Green : ExitCode := ExitCode + 1 |
    Blue  : ExitCode := ExitCode + 2
  END
END Sets.
./M2c sets.mod
$MCINT Sets.MC4

Output:

16

10. Example 8 — Local modules

A local MODULE M with an EXPORT list hides locals and exposes M.x / M.Proc to the enclosing program. Optional BEGIN ... END init bodies in modules run at startup, before the program's own statements, in declaration order.

MODULE MMod;

MODULE M EXPORT cnt, Inc;      (* local module, exports 2 names *)
VAR cnt : INTEGER;
PROCEDURE Inc;
BEGIN
  cnt := cnt + 1
END Inc;
END M;

VAR ExitCode : INTEGER;
BEGIN
  M.cnt := 10;
  M.Inc();          (* exported call *)
  M.Inc();
  ExitCode := M.cnt (* 12 *)
END MMod.
./M2c mmod.mod
$MCINT MMod.MC4

Output:

12

11. Example 9 — Separate compilation units

The V1 compiler compiles a library split across a definition and an implementation, plus a client — all in one session — into a single image. This is the step-11 feature; the library's BEGIN init runs before the program body.

mathlib.def:

DEFINITION MODULE MathLib;
CONST PiSq = 9;
VAR calls : INTEGER;
PROCEDURE Square(x : INTEGER) : INTEGER;
PROCEDURE Bump(VAR x : INTEGER);
END MathLib.

mathlibimpl.mod:

IMPLEMENTATION MODULE MathLib;
PROCEDURE Square(x : INTEGER) : INTEGER;
BEGIN
  RETURN x * x
END Square;
PROCEDURE Bump(VAR x : INTEGER);
BEGIN
  x := x + 1
END Bump;
BEGIN
  calls := 0                     (* module init: runs first *)
END MathLib.

app.mod:

MODULE App;
FROM MathLib IMPORT Square;      (* unqualified import *)
VAR total : INTEGER;
VAR ExitCode : INTEGER;
BEGIN
  total := Square(3) + MathLib.PiSq;   (* 9 + 9 = 18 *)
  MathLib.Bump(total);                 (* qualified call, VAR param *)
  ExitCode := total + MathLib.calls    (* 19 + 0 *)
END App.

Compile all three files on one command line — definitions first, then the implementation, then the program module:

./M2c mathlib.def mathlibimpl.mod app.mod
$MCINT App.MC4

Output:

19

From mathlib.def, the imported name is Square (procedure), the qualified access is MathLib.PiSq (constant) and MathLib.Bump (procedure); .LST files are written for every unit. A mismatch between an implementation heading and its definition is error 231.


12. Example 10 — When things go wrong

Say you assign an INTEGER to a BOOLEAN:

MODULE BadTest;
VAR i : INTEGER;
VAR b : BOOLEAN;
VAR ExitCode : INTEGER;
BEGIN
  i := 1;
  b := i;                    (* wrong: BOOLEAN := INTEGER *)
  ExitCode := i
END BadTest.
./M2c badtest.mod

stdout ends with:

Incorrect source

For scripting: M2c currently 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.

and badtest.LST shows the offending line with a caret and message:

Listing:

    1  MODULE BadTest;
    2  VAR i : INTEGER;
    3  VAR b : BOOLEAN;
    4  VAR 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, 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.


13. Language gotchas and limits

  • Identifiers are only letters and digits — no underscores. pi_sq does not lex; use PiSq.
  • The only text output is WriteString("...") and WriteInt(n); the standard result channel is the ExitCode convention.
  • A single-character literal is a CHAR, not a string.
  • INTEGER DIV/MOD truncate toward zero; AND/OR are eager (no short-circuit); mixed INTEGER/REAL arithmetic is rejected (use assignments to widen).
  • Array indexing is not range-checked, and NIL dereference reads 0 — no runtime trap yet.
  • Current caps: 64 procedures, 64 actuals per call, 16 names per parameter section, 8-deep nested calls, 8-deep WITH, one image per program (no multi-.MC4 linkage, no circular imports, no opaque types).
  • Everything that is parsed but not lowered yet reports error 230 — the compiler deliberately keeps a clean single-pass semantic model.

14. Where to go next

  • src/Showcase*.mod — four "tour" programs exercising the whole language (157, 83, 168 and the step-11 225).
  • docs/summary_step*.md — one development-step summary each (what was added, test counts, bugs found).
  • README.md — full feature list, error-code table, layout and build details.
  • tests/ — the 158 programs of the regression suite used by run_tests.sh; a great source of runnable examples for every construct.