A Gentle Introduction to Modula-2

(Last updated: 23 November 1999. Please send comments to: mcheng@csr.uvic.ca.)

Contents

  1. Background
  2. Introduction
  3. Basic Compilation Units
  4. Use Multiple Modules
  5. Nested Modules
  6. Module Initialization
  7. Data and Control Structures
  8. Coroutines
  9. TopSpeed Modula-2 Command Line Features

Background

This note is written for those who are learning Modula-2 for the first time and have done some programming in C/C++/Java. It is not intended to be a Modula-2 language manual nor a comprehensive Modula-2 programming guide. For those who would like to learn more about Modula-2, please read the following references: There are numerous books on programming with Modula-2. I am sure you'll find many good references on this subject in our library or bookstores. We do have some copies of "TopSpeed Modula-2: Language Tutorial" by K.N.King available in room ELW-A209.

Introduction

Modula-2 is designed to be a systems programming language (e.g., compilers, operating systems, embedded systems, etc.) and a successor to Pascal to support programming-in-the-large. It is not an object-oriented programming language. But it has many features to make it suitable for large programming projects. The main features of Modula-2 include the support of: The above features are completely missing in Pascal. C supports separate compilation and module decomposition using included files. C's included files feature provides a crude support for module interfaces. For systems programming, bit manipulation is quite common in dealing with machine words/registers. C supports binary bit operations on integers. It is the programmer's responsibility to convert (hexadecimal or binary) integers into bit-field operations. C doesn't support multithreading at all. It is typically supported by libraries of the underlying operating systems.

C++/Java, on the other hand, are designed to support large scale object-oriented programming projects. The concept of classes is in many ways similar to modules in Modula-2. A class is a template for defining the interface and behavior of objects. A module is a software component with a well-defined interface and implementation. Every instance of a class is an object, but every module defines a single-instance component, itself. To support multiple instances, a module may encapsulate abstract data types (e.g., a stack, a binary tree, a queue). A module is a single-instance software component (e.g., a library, an operating system, a compiler) with a well-defined application programming interface (its API). Single-instance classes, interface classes and packages are basically modules. Modules in Modula-2, however, do not support inheritance, only encapsulation. Hence, Modula-2 is not an object-oriented programming language.

Basic Compilation Units

A program in Modula-2 consists of one or more modules. There are four types of modules:
  1. a main module --- our main program (e.g., the main() function in C or class main in Java),
  2. a sub module --- a module which nests inside another main, sub  or implementation module,
  3. a definition module --- the interface definition of a component, and
  4. an implementation module --- a module which implements some definition module.
Every program must have exactly one main module. By convention, the name of a main module should be the same as the name of the program. For example, the following is a simple main module Hello.
MODULE Hello;
  (* declarations  of Hello *)
BEGIN
  (* main body of Hello (* with nested comments *) *)
END Hello.
All reserved keywords in Modula-2 must be in UPPER-CASE letters. Comments are enclosed by "(*" and "*)", which may be nested (i.e., comments may enclose other comments as long as they are properly enclosed by the delimiters.)

Pre-defined components (such as a graphics library, an IO library, a queue abstract data type, a window manager, etc.) are built out of definition and implementation modules. A definition module specifies a publicly accessible API of the component, while the implementation module provides the actual implementation of the component. For a given API (a definition module), one may provide different implementations for it as long as they all "satisfy" the same API requirements.

A definition module should be regarded as a "contract" between the users and implementers of a component; it defines what a user may have access to and what the implementer must provide. Once the API is defined, the users and implementers of the component may go their separate ways. The user may use the component by importing its interface. It is the responsibility of the implementer to ensure that the supplied implementation satisfies the API. In practice the interface of a component doesn't change much once it has been fully and thoroughly specified. It is usually the actual implementation that may change from time to time in order to fix errors and/or to improve performance. It is not uncommon to see several implementations for a single API for different platforms. Building components out of well-defined interfaces and implementations is one of the main principle of module decomposition in programming-in-the-large.

In Modula-2, the API of a component is specified by a definition module. For example, the following is an example of a simple stack definition module.

DEFINITION MODULE SimpleStack;
  PROCEDURE Push( i : INTEGER ); (* push an INTEGER i on top of the stack *)
  PROCEDURE Pop() : INTEGER;     (* remove and then return the top element of the stack *)
  PROCEDURE Empty() : BOOLEAN;   (* TRUE if the stack is empty *)
END SimpleStack.
(Note: There is a keyword DEFINITION in front of the MODULE keyword.) This SimpleStack module provides three access procedures and supports INTEGER data type only. A definition module specifies the publicly accessible procedures, variables and data types of a component; thus, it shouldn't contain any executable code. Everything specified inside a definition module is publicly accessible. Private variables, data types and procedures should be provided in the implementation module instead. This particular SimpleStack definition module specifies a single-instance stack, i.e., at most one stack may be used. The initialization of the internal state of a SimpleStack could be achieved in the main body of its implementation module. (See the section Module Initialization for details.)

A more flexible stack definition supporting multiple IntStack instances may be specified as follows.

DEFINITION MODULE IntStack;
  TYPE
     anIntStack; (* an opaque pointer type *)
  PROCEDURE New( VAR s : anIntStack ); (* allocate a new anIntStack *)
  PROCEDURE Push( VAR s : anIntStack; i : INTEGER );
  PROCEDURE Pop( VAR s : anIntStack ) : INTEGER;
  PROCEDURE Empty( s : anIntStack ) : BOOLEAN;
END IntStack.
This IntStack module encapsulates an (opaque) abstract data type called anIntStack, whose internal representation is completely hidden from its users. A user is allowed to declare variables of type anIntStack, but he/she cannot see how anIntStack is represented, and thus cannot inadvertently modify its contents. Furthermore, a user may create as many anIntStack as required. Each instance is denoted by a variable of type anIntStack and is independent of one other. Each call to New() will allocate a new instance of anIntStack. (* Note: the keyword VAR denotes a call-by-reference parameter, i.e., the input argument may be modified.)  Modula-2 doesn't support inheritance nor generic templates. Hence, you cannot easily define a generic stack module in Modula-2 independent of its data values, i.e., INTEGER. Question: How is anIntStack represented? And where? What is an opaque type?

To fulfill our "contract" of IntStack, we need provide an implementation. There are many ways to implement a stack data type, e.g., a single-linked list, or a fixed size array. This implementation decision is of no concern to  users of IntStack. Therefore, we hide it inside our implementation module, which is normally unavailable in source form to any user.

IMPLEMENTATION MODULE IntStack;
  CONST
      MAX = 10;
  TYPE
       (* our opaque type is represented and defined here *)
     anIntStack = POINTER TO aStack; (* an opaque type must be a POINTER type *)
     aStack = RECORD stk : ARRAY [1..MAX] OF INTEGER; count : INTEGER; END;
  PROCEDURE New( VAR s : anIntStack );
  BEGIN
    (* allocate a new RECORD for "s" and then initialize "count" *)
    ...
  END New;

  PROCEDURE Push ...  (* code for Push *)
  PROCEDURE Pop ...  (* code for Pop *)
  PROCEDURE Empty( s : anIntStack ) : BOOLEAN;
  BEGIN
    RETURN ( s^.count = 0 );
  END Empty;
BEGIN (* IntStack *)
  (* nothing *)
END IntStack.

(Note: There is an IMPLEMENTATION keyword in front of MODULE.) For this particular implementation, we decided to use a fixed array to represent our anIntStack. This is not ideal, but may be sufficient for simple applications. When we compile this implementation module, the compiler will automatically look for the DEFINITION module of the same name. One may provide several implementations of the IntStack definition as long as they are not all available at the same time or at the same place. Each implementation module is compiled into a relocatable object module, which is ready for linking.

(Remarks: For TopSpeed Modula-2, a definition module has a file suffix of ".DEF", and a main/ implementation module has a file suffix of ".MOD". Make sure that you have exactly a single main module per application built; otherwise, you will get strange linking errors. For our IntStack example, assume that the IntStack definition is stored in the file "IntStack.DEF" and its implementation is in "IntStack.MOD". After we compile this module, we will obtain an object module called "IntStack.OBJ". The files "IntStack.DEF" and "IntStack.OBJ" are all a user needs in order to build an application using IntStack. These two files form a "black-box" component of IntStack; only its API and its linkable object code are available, and nothing else. This component may be reused in many applications as long as the API remains unchanged. )

Using Multiple Modules

Program development may be top-down, bottom-up or mixed. A top-down development process begins with a top-level module, identifies its requirements and subdivides its subcomponents into definition modules, and then repeats the process on the required implementation modules. On the other hand, a bottom-up development process begins with a set of pre-defined and pre-built components, identifies which components may be used to build higher-level components towards the program's goals, and then repeats until the highest-level component defines our main program. In reality, program development is never that straightforward, i.e., a linear top-down or bottom-up process. One always defines new components as needed and reuses existing components where appropriate.

To use an existing component, we need its API. In Modula-2, we import the definition of a module/component. The word "import" means "use". Often we don't need the entire component, just a few types or access procedures provided by the API. In this case, we may selectively import what is needed. A "smart" linker will link into our application of what we need and leave behind those that we don't. "import" doesn't mean "include", i.e., textual inclusion. It does no harm if we import the same API several times; it is the same as saying "we use a component" several times. By stating that we use a component by importing its API, we declare that our application "depends" on the supplied API and its implementation. If, for some reason, the required API has been modified, then we would like to be informed about such changes and re-check our usage for consistency.

In TopSpeed Modula-2, there are many pre-built library components. For example,

There are separate ".DEF" and ".OBJ" files for each module. (Please check the supplied definition module files for details.) The definition files are the APIs, and the object files are linkable pre-compiled object modules. To use any of these libraries, we need to import them. For example,
MODULE Hello;
  IMPORT IO;
  (* declarations  of Hello *)
BEGIN
  IO.WrStr( "Hello world!" ); IO.WrLn();
END Hello.
this main program imports the IO module and uses the WrStr() and WrLn() procedures. When this main Hello module is compiled, the API of IO will be consulted automatically checking for consistency of usage. If successful, the "Hello.OBJ" is produced and could later be linked with "IO.OBJ" to produce the final executable "Hello.EXE". You don't need to supply any project file or make file in order to specify the dependency of Hello on IO. The IMPORT statement specifies such a dependency relationship and will be used by the compiler and the linker appropriately. (Please check the section on TopSpeed Modula-2 Command Line Features for details.)

The IO module provides many other input/output procedures. The Hello module only uses two. We could have written Hello as follows.

MODULE Hello;
  FROM IO IMPORT WrStr, WrLn;
  (* declarations  of Hello *)
BEGIN
  WrStr( "Hello world!" ); WrLn();
END Hello.
In the above example, we selectively import WrStr() and WrLn() procedures only from module IO. We are not interested in any other procedures in IO. Notice that we don't need to prefix the WrStr() and WrLn() procedure calls with the prefix IO since we have already specified where they come from in the IMPORT statement. Qualified IMPORT statement (using the FROM keyword) commits any future references to a specific module, but potentially can create ambiguity when references of the same name are imported from different modules. (I strongly recommend using the unqualified IMPORT statements and prefixing any external references by their module names. It is easier to trace any external reference to its source module.)

The following IntStack implementation module uses the Storage module to support its anIntStack data type.

IMPLEMENTATION MODULE IntStack;
  IMPORT Storage; (* we need dynamic storage allocation/deallocation *)
  CONST
      MAX = 10;
  TYPE
     anIntStack = POINTER TO aStack; (* an opaque type must be a POINTER type *)
     aStack = RECORD stk : ARRAY [1..MAX] OF INTEGER; count : INTEGER; END;
  PROCEDURE New( VAR s : anIntStack );
  BEGIN
    (* allocate a new RECORD for "s" and then initialize "count" *)
    Storage.ALLOCATE( s, SIZE(aStack) );
    s^.count := 0;
  END New;

  PROCEDURE Push ...  (* code for Push *)
  PROCEDURE Pop ...  (* code for Pop *)
  PROCEDURE Empty( s : anIntStack ) : BOOLEAN;
  BEGIN
    RETURN ( s^.count = 0 );
  END Empty;
BEGIN (* IntStack *)
  (* nothing *)
END IntStack.

(Note: We don't need to import the definition module IntStack explicitly. Because this is an implementation module of IntStack, the compiler will automatically import the definition module IntStack.) To complete our example application,  we supply a main module which uses IntStack as follows.
MODULE UseStack;
  IMPORT IntStack, IO;
  VAR
     s : IntStack.anIntStack;
     i : INTEGER;
BEGIN (* main body of UseStack *)
  IntStack.New( s );
  IntStack.Push( s, 1 );
  IntStack.Push( s, 2 );
  WHILE NOT IntStack.Empty( s ) DO
    i := IntStack.Pop( s );
    IO.WrInt( i, 5 ); IO.WrLn();
  END;
END UseStack.
The UseStack main module uses IntStack and IO modules, and the implementation module of IntStack uses Storage module. As long as we don't change the API of IntStack, even if we change the implementation of IntStack, we don't need to re-compile UseStack. We only need to re-link in order to produce a new UseStack.EXE file.

Nested Modules

A sub-module may be nested inside a main module, an implementation module, or another sub-module, but not inside definition module. A nested sub-module is "local" to the enclosing module. Without explicitly exporting its API, a sub-module is a complete "useless" black-box. Any sub-module may be made into a separate independent module by making its export API into a definition module and turning its code into an implementation module. The main purpose of a nested sub-module is to protect from unintended usage, i.e., inadvertently imported by another module. Such concept of locally-owned modules is useful to break a large implementation up into manageable subparts without undue concern of interferences by other modules, accidentally being called by an outside module.

For example, the following is a simple OS with two sub-modules, Queues and Semaphores.

IMPLEMENTATION MODULE OS;
  IMPORT Storage; (* for sub-modules Queues and Semaphores *)

  TYPE
    Process = POINTER TO PD;
    PD = RECORD ... END;

  MODULE Queues;
    IMPORT Process, Storage;
    EXPORT Enqueue, Dequeue, aQueue;
    TYPE aQueue = POINTER TO Process;
    PROCEDURE New( VAR q : aQueue );
    PROCEDURE Enqueue( VAR q : aQueue; p : Process );
    PROCEDURE Dequeue( VAR q : aQueue; VAR p : Process );
    PROCEDURE Empty( q : aQueue ) : BOOLEAN;
  BEGIN (* Queues *)
    (* the main body of Queues *)
  END Queues;

  MODULE Semaphores;
    IMPORT Process, PD, Storage;
    EXPORT aSemaphore, InitSem, Signal, Wait;
    TYPE   aSemaphore = RECORD ... END;
    PROCEDURE New( VAR s : aSemaphore );
    PROCEDURE Signal( VAR s : aSemaphore );
    PROCEDURE Wait( VAR s : aSemaphore );
  BEGIN Semaphores;
    (* main body of Semaphores *)
  END Semaphores;

BEGIN (* OS *)
  (* the main body of OS *)
END OS.

The sub-modules are completely owned and used by the enclosing module OS as long as OS doesn't export any internal API of Queues and Semaphores to the outside. The EXPORT statements define what are accessible by the enclosing module. Anything not stated in the EXPORT statements are private to the sub-module. Using sub-modules, logically related types and procedures may be grouped to form a self-contained sub-component, thus enhancing the readability and maintainability of large modules. (Notice that the IMPORT statements inside a sub-module refer to names other than modules, such as global variables and types.)

Module initialization

An implementation module, main module or sub-module have an optional main body. The main body of such a module is automatically executed before any part of the module is called or referenced. Hence, the main body of a module is usually reserved for the often necessary internal module state initialization. Sub-modules are initialized first before the enclosing modules. Dependent modules are initialized before the depending modules. Module IMPORT statements specify a hierachical module dependency acyclic graph. The modules at the bottom are initialized first before the higher level ones. As a result, the top-level main module is always initialized last. Every module is initialized exactly once when a program starts. During initialization, the main body of a module must terminate successfully before the next one starts. The main body of the main (or top-most) module always executes last; when it terminates, the whole program terminates. The automatic invocation of the main() function in C is akin to the self-initialization of the main body of the main module; when main() aborts/terminates, the whole program aborts/terminates. Self-initialization of modules is a unique feature in Modula-2.
IMPLEMENTATION MODULE SimpleStack;
CONST
  MAX = 100;
VAR
  Stk : ARRAY [1..MAX] OF INTEGER;
  Count : CARDINAL;
BEGIN (* main body *)
  Count := 0;
END SimpleStack.
For example, in this SimpleStack implementation, the main body will be executed automatically before any of the access procedures is being called. Hence, the user of this module doesn't need to invoke an initialization procedure upon program startup.

Data and Control Structures

Modula-2 is a strongly-typed language and supports the traditional data structuring features, arrays, records/structures and pointers. Basic types include INTEGER (negative and positive numbers), CARDINAL (positive numbers including 0), CHAR and BOOLEAN. (TopSpeed Modula-2 also supports 8-bit data types, e.g., SHORTINT, SHORTCARD, and 32-bit data types, e.g., LONGINT and LONGCARD.)

Enumerated types, index types, subrange types and procedure types are supported. One may define a variable of type procedure. Procedures may be assigned to procedure variables of the same type. Open arrays may be specified as paramters of procedures, e.g., ARRAY OF INTEGER denotes a parameter of an unbounded array of INTEGER. An open array begins with an index 0 (similar to C). The standard function HIGH(a) returns the index of the last element of an open array "a". The length of an open array "a" is always HIGH(a) plus 1.

For example,

    MODULE Example;
    TYPE
        Bound = [1..100];  (* an index type *)
        aName = ARRAY Bound OF CHAR;  (* an array type *)
        Day = (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday);
        WorkDay = [Monday..Friday];  (* a subrange type *)
        PersonPtr = POINTERTO Person;
        nextOfKind = PersonPtr;
        Person = RECORD
                    age : CARDINAL;
                    sex : (Male, Female);
                    name : aName;
                    father, mother, siblings : nextOfKin;
              END;

        aCommand = PROC( n : ARRAY OF CHAR; VAR p : PersonPtr);
    VAR
        request : aCommand;

    PROCEDURE Inquire ( n : ARRAY OF CHAR; VAR p : PersonPtr );
    BEGIN
      (* body of Inquire *)
    END Inquire;

    VAR
        p : PersonPtr;
    BEGIN (* main body *)
        request := Inquire; (* assign a procedure to a variable *)
        request( "joe", p ); (* invoke a procedure variable *)
    END Example.

For low-level systems programming, the type WORD denotes a memory word which is compatible with any type of the same size, the type ADDRESS is a pointer to any WORD and thus is compatible to any pointer types, and the type BITSET denotes a set of WORD size number of bits. Two standard functions INCL and EXCL are predefined for BITSET manipulation.

    TYPE
        Register = BITSET;
    VAR
        ctrl : Register;
    BEGIN
        ctrl := ctrl + {0}; (* setting the least-significant bit, bit-0 *)
        ctrl := ctrl - {15}; (* clearing the most-significant bit *)
        IF 3 IN ctrl THEN (* bit-3 is set in ctrl *)
            ctrl := ctrl - {3,7}; (* clear both bit-3 and bit-7 *)
            INCL( ctrl, 2 ); (* ctrl := ctrl + {2} *)
        ELSE
            ctrl := ctrl + {4,5}; (* set both bit-4 and bit-5 *)
            EXCL( ctrl, 0 ); (* ctrl := ctrl - {0} *)
        END;
    END;

A variable (of any type) may be declared to be at a fixed memory location. For example,

    VAR
        status [0000:0032] : Register;

declares a "status" register at memory location 0000:0032 (segment:offset). Thus, any read/write operation performed on status will be done in the specified memory location.

Modula-2 supports all conventional iterative control structures, e.g., while, for, repeat-until and loop-exit. The EXIT statement inside a LOOP-END statement always exits the inner-most loop. All boolean expressions are evaluated in a short-circuited manner. That is,  "A AND B" means "IF A THEN (IF B THEN TRUE ELSE FALSE) ELSE FALSE"; hence, if A is false, then B is never evaluated. Similarly, "A OR B" means "IF A THEN TRUE ELSIF B THEN TRUE ELSE FALSE"; if A is true, then B is never evaluated. C/C++/Java all evaluate boolean expressions similarly.

Coroutines

Modula-2 supports multi-threaded programming. The supported mechanism is called "coroutines". Procedures are also known as subroutines. When a procedure A calls another procedure B, then A's execution is stopped where the call to B is made until B returns. Such execution must be sequential and properly nested. If B never returns, then A will never resume. If A is a command loop procedure and B is a graphic operation which takes a long time to complete, then while B is executing upon a user command invoked by A, A cannot accept another command (e.g., to abort the command B). This is an example where we need multi-threading, the support of concurrent execution. Ideally, A could send a user request to B, and then waits for further user inputs or a result returned from B. That is, A and B are executing independently and concurrently. A "thread" is an encapsulated unit of computation which is independently schedulable by the underlying operating systems or the runtime systems. (Note: A thread is sometimes also known as a light-weight process. I will use the terms threads and processes interchangeably.)

Q. How could we suspend/resume a process at will? What is a process?

     process = code + context

where a context includes instruction pointer/program counter, registers, state of global and local variables. Several processes may be executing the same piece of code as long as they use different contexts. The context represents the current state of a process.

In Modula-2, the NEWPROCESS primitive (defined in the module SYSTEM) allows one to create a process from a given procedure.
 

PROCEDURE NEWPROCESS( p : PROC; workspace : ADDRESS; wksize : CARDINAL;
                   VAR ctx : ADDRESS );


where "p" is a parameter-less procedure in Modula-2, "workspace" is a pointer to a piece of memory holding the context, "wksize" is the size of "workspace" in bytes, and finally "ctx" is the context that NEWPROCESS returns as a result of turning "p" into a process. The context variable "ctx" will be used by TRANSFER and IOTRANSFER for switching from one process to another. (Note: The type ADDRESS is a universal pointer type, similar to "void *" in C.)

  PROCEDURE TRANSFER( VAR me, you : ADDRESS );

The parameters "me" and "you" are of type ADDRESS, which are not any addresses, but a context address created by NEWPROCESS. Informally, the behaviour of TRANSFER can be summarized as follows.

  PROCEDURE TRANSFER( VAR me, you : ADDRESS );
    (*
     * Transfer the control (of the cpu) from "me" to "you",
     * where "me" denotes context of the caller of TRANSFER.
     * When the control is later returned to "me" the execution is
     * resumed at where TRANSFER was called.
     *)
  VAR
    tmp : ADDRESS; (* a temporary context ADDRESS *)
  BEGIN
    tmp       := you;
    me.state  := cpu.state; (* save current cpu state in "me" *)
    cpu.state := tmp.state; (* switch cpu state to "you" *)
     (*
      * At this point the control of the cpu is being passed to
      * "you" and execution continues in "you". When the control
      * is later returned to "me", the execution continues from here!
      *)
  END TRANSFER;

Question: What do the following statements do?

  VAR x : ADDRESS;

  TRANSFER( x, x );

Consider the following program fragment:

  MODULE M;
    FROM SYSTEM IMPORT NEWPROCESS, TRANSFER, ADDRESS, BYTE;
    CONST
      WKSIZE = 512;
    VAR
      wkspA, wkspB : ARRAY [1..WKSIZE] OF BYTE;
      main, cA, cB : ADDRESS;
      x : ADDRESS;     (* a shared context variable *)

    PROCEDURE A;
    BEGIN
      LOOP
        ...
        TRANSFER(x,x);
        ...
      END;
    END A;

    PROCEDURE B;
    BEGIN
      LOOP
        ...
        TRANSFER(x,x);
        ...
      END;
    END B;

  BEGIN (* M *)
      (* create two processes out of procedure A and B *)
    NEWPROCESS( A, ADR(wkspA), WKSIZE, cA );
    NEWPROCESS( B, ADR(wkspB), WKSIZE, cB );
    x := cB;
    TRANSFER(main,cA);
  END M.

The execution of "TRANSFER(x,x)" statements in "A" and "B" may be explained as follows:

    (* when TRANSFER(main,A) is executed *)
  tmp        := A;
  main.state := cpu.state;
  cpu.state  := tmp.state;
    (* "main" is suspended; "A" is now executing *)
  ...
    (* x = "B" initially *)
    (* when TRANSFER(x,x) in "A" is executed *)
  tmp        := x;          (* tmp = "B" *)
  x.state    := cpu.state;  (* x = "A" *)
  cpu.state  := tmp.state;  (* switch to "B" *)

    (* at this point, "B" is executing and x is "A" *)
  ...
    (* when TRANSFER(x,x) in "B" is executed *)
  tmp        := x;          (* tmp = "A" *)
  x.state    := cpu.state;  (* x = "B" *)
  cpu.state  := tmp.state;  (* switch to "A" *)

  (* at this point, "A" is executing and x is "B" *)

Therefore, the TRANSFER(x,x) statement allow two processes to pass control back and forth. Note the subtlety in the exchange of states within TRANSFER.

(Note: Before you read any further, please read "A note about 80x86 Interrupt Architecture".) TRANSFER achieves coroutining between two procedures, i.e., the transferring CPU control from one process to another. Q. What if a process is a "device handler"? That is, a process responds to the events generated by the hardware, e.g., timers, disk controllers, keyboard, etc. A device handler is typically a pseudo process which is invoked, or is given the control of the CPU, upon an occurrence of an event generated by the associated device. The primitive IOTRANSFER is designed to establish a connection between a device handler and its associated device via the interrupt vectors. For Intel 80x86 architecture, there are 256 interrupt vectors occupying the first 1KB of physical memory. The meaning of IOTRANSFER can be summarized similarly as follows.

  CONST
    MAX_INT_TYPE = 256;   (* on the 80x86 processors *)

  TYPE
    INT_NUMBER = [0..MAX_INT_TYPE-1];  (* interrupt type number *)

  VAR
      (* this occupies the first 1K of memory addresses *)
      (* In Modula-2, the notation "x [addr] : T" means the variable
       * "x" of type T is declared to be at absolute memory
       * location "addr". And, an ADDRESS in TopSpeed Modula-2
       * is 4 bytes.
       *)
    InterruptVectors [0000:0000] : ARRAY INT_NUMBER OF ADDRESS;

  PROCEDURE IOTRANSFER( VAR handler, current : ADDRESS; n : INT_NUMBER );
    (*
     * Install the "handler" for the interrupt type "n" and transfer
     * control (of the cpu) to "current".
     *
     * When an interrupt of type "n" occurs, the state of the cpu is
     * saved in "current" and the control is transferred back to the
     * "handler", the point where IOTRANSFER was called.
     *)
  VAR
    saved : ADDRESS;  (* saved interrupt vector *)

  BEGIN
    saved               := InterruptVectors[n]; (* save the old vector *)
    InterruptVectors[n] := handler;     (* now, install handler *)

    handler.state       := cpu.state;   (* save the handler's state *)
    cpu.state           := current.state;
    (*
     * At this point, "current" is executing. When an interrupt of type
     * "n" occurs, control is returned here via InterruptVectors[n]!
     *)
    current.state       := cpu.state;     (* save current state *)
    cpu.state           := handler.state; (* restore the handler's state *)
    InterruptVectors[n] := saved;         (* restore old interrupt vector *)
  END IOTRANSFER;

Typically, an interrupt handler installs itself using IOTRANSFER as follows.

  CONST
    WK_SIZE = 512;
  VAR
    wkSp : ARRAY [1..WK_SIZE] OF BYTE;
    main, handler : ADDRESS;

  PROCEDURE InterruptHandler; (* for interrupt type "n" *)
  BEGIN
    LOOP
        (* after calling IOTRANSFER, it is suspended! *)
      IOTRANSFER( handler, main, n );
        (* when an interrupt of type "n" occurs, execution resumes here! *)
      ... (* the rest of interrupt handler code *)
    END;
  END InterruptHandler;

  BEGIN (* main *)
      (* install interrupt handler for interrupt type "n" *)
    NEWPROCESS( InterruptHandler, ADR(wkSp), WK_SIZE, handler );
    TRANSFER( main, handler );
  END.

Notice that each time IOTRANSFER returns, the interrupt handler is deinstalled from the interrupt vector. Therefore, the IOTRANSFER call in the interrupt handler must be executed inside a LOOP so that it will reinstall itself after each interrupt is serviced.

TopSpeed Modula-2 Command Line Features

The TopSpeed Modula-2 has a builtin "auto-make" feature, i.e., it automatically checks the time-stamps of all definition and implementation modules, and their object modules used by the main modules. If the time-stamps are inconsistent, the "newer" implementation modules will be recompiled and the main program will be relinked to produce a "newer" version. This is similar to the "make" facility on Unix or in most programming environments. You don't need to specify the dependency relationship of all modules in a separate "make" file. The import hierarchy is all that is needed.

To ensure every assignment/project is being evaluated under identical conditions, we shall recompile all source files with the following compiler options:

m2/c <main module> /b /v /f /m and link all objects with the following linker options: m2/l <main module> /i /c We don't usually turn on debugging information. But, if you intended to use the "vid" debugger, then you should also include /d as a compiler option and /m as a linker option.

If you just type "m2", you'll enter the TopSpeed Modula-2 Programming Environment, which has a builtin multi-file editor, a compiler and linked.
If you use the programming environment to generate your application (rather than using command line invocation of compile and link), you should check the Compiler and Linker Options and make sure they are consistent with the recommended options.

Please note that the TopSpeed Modula-2 Programming Environment remembers previous settings. So be sure that you check the Options before you start compiling your programs.