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.
MODULE 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.)
(* declarations of Hello *)
BEGIN
(* main body of Hello (* with nested comments *) *)
END Hello.
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;(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.)
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.
A more flexible stack definition supporting multiple IntStack instances may be specified as follows.
DEFINITION MODULE 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?
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.
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;(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.
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.
(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. )
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,
MODULE 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.)
IMPORT IO;
(* declarations of Hello *)
BEGIN
IO.WrStr( "Hello world!" ); IO.WrLn();
END Hello.
The IO module provides many other input/output procedures. The Hello module only uses two. We could have written Hello as follows.
MODULE 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.)
FROM IO IMPORT WrStr, WrLn;
(* declarations of Hello *)
BEGIN
WrStr( "Hello world!" ); WrLn();
END Hello.
The following IntStack implementation module uses the Storage module to support its anIntStack data type.
IMPLEMENTATION MODULE 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.
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.
MODULE 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.
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.
For example, the following is a simple OS with two sub-modules, Queues and Semaphores.
IMPLEMENTATION MODULE 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.)
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.
IMPLEMENTATION MODULE 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.
CONST
MAX = 100;
VAR
Stk : ARRAY [1..MAX] OF INTEGER;
Count : CARDINAL;
BEGIN (* main body *)
Count := 0;
END SimpleStack.
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.
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.
To ensure every assignment/project is being evaluated under identical conditions, we shall recompile all source files with the following compiler options:
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.