Terra Datentechnik Home Home · Modula-2

Stony Brook Modula-2 language extensions

This page briefly goes over various Stony Brook Modula-2 language extensions

Additional predefined types


These types are pervasive just like INTEGER and CARDINAL.

These types can also be imported from the SYSTEM module. Doing this documents the type as an extended feature in your source if this concerns you.


A new pervasive identifier NILPROC is introduced, denoting a constant procedure value and having the type NILPROC-TYPE.

  • NILPROC-TYPE is assignment compatible to every procedure type.
  • NILPROC-TYPE is compatible to every procedure type in the comparison operations "=" and "<>".
  • A call to a procedure variable having the value NILPROC shall raise the exception "invalidLocation".

Unicode support

  • The compiler supports both ASCII character and Unicode character and string types. The definition of the CHAR type can be either ASCII or Unicode. This is changed with a compiler option. The compiler supports the ACHAR and UCHAR types, which are ASCII and Unicode respectively.
  • Ascii and Unicode string literals. By default a string literal element type is of type CHAR. You can declare a specific string literal type as ASCII or Unicode. The compiler converts string constant to/from ASCII and Unicode as necessary.
  • The ACHR and UCHR pervasive functions complement the existing CHR pervasive function and provide function that return an ASCII or Unicode character type respectively.

Bitwise Operators


The operators perform bitwise operations. They can be used with all cardinal and integer types.

Dynamic array types

Dynamic arrays are dynamically allocated types and are accessed via pointer dereferencing. Dynamic arrays are arrays of a constant number of dimensions, but the size of the individual dimensions is unknown at compile time. Dynamic arrays have their best use on arrays with more than one dimension.

To specify dynamic array types, use:




Dynamic arrays are indexed the same as open array parameters, where the lower subscript bound is assumed to be zero and the upper bound is a runtime value that can be read with the HIGH built-in function, and the type of the subscript is CARDINAL.

Allocating dynamic arrays

Dynamic arrays are set by allocating a block of memory for the object that they point to.  The NEW standard procedure. You can deallocate the memory with the DISPOSE standard procedure. The NEW and DISPOSE procedures are discussed in the standard procedures section of this document. Example:

NEW(MyMatrix, 5, 7);

Extended loop control

  • BREAK statement. This statement causes the current loop to be exited. This statement is allowed with all Modula-2 loop constructs.
  • CONTINUE statement. This statement starts the next iteration of the current loop. In FOR loops, the loop counter is adjusts. In REPEAT, WHOLE and LOOP constructs this statement branches to the top of the loop. This statement is allowed with all Modula-2 loop constructs.
  • Named loops. This extension is just like the Ada syntax. You can name a specific loop and use the BREAK and CONTINUE statements to exit a specific loop by name.
    FOR i := 0 TO 9 DO
       FOR j := 0 TO 9 DO
          IF ... THEN
             BREAK OuterLoop; (* exits the "i" loop *)
    (* BREAK OuterLoop transfers control here *)

Support for assembly code

The compiler contains a built-in assembly language parser.

  • You can embed assembly code in a Modula-2 procedure. This is strongly discouraged.
  • You can declare procedure which are implemented in assembly code rather than Modula-2. There are two forms of assembly procedures.
    1. ASSEMBLER procedures. The compiler does all of the tedious work of generating the procedure prologue and epilog. You can declare local variables and the compiler will allocate them for you. You only write the “guts” of the procedure.
    2. PUREASM procedures. The compiler generates nothing. You have complete control.

Support for definition module “macros”.

The MACRO modifier is only allowed in DEFINITION modules and it signals that a procedure body follows the header. This procedure code will be generated inline at each occurrence it is called. You are not allowed to declare nested procedures or modules within a macro. You are allowed to declare local types, constants and variables.




    RETURN a + b;
END add;


END MyModule.

Procedure Attributes

Procedure attributes are a mechanism that allows you to control and override the various aspects of how procedures are called and linked. This allows you to interface with any other system in existence.

Variable number of parameters.

You can call procedures, which accept a variable number of parameters, and you can declare your own procedures, which accept a variable number of parameters.

Variable declaration extensions

  • You can change the public symbol name of any variable for easy interfacing with other languages. Example:
    VAR MyVar [“PublicName”] : CARDINAL;
  • Initialized variables. You can declare variables with an initial value. Global variables are initialized at program start, and procedure variables are initialized on entry to the procedure. Example:
    VAR MyVar : CARDINAL = 23;

Enumeration syntax support to allow easy interfacing with the C language.


enum = (one, two = 5, three);

In the above enumeration the ordinal value of the identifier "three" is six.

GtkPathPriorityType =

Enumeration type storage allocation adjustment.


TYPE enum = (one, two, three) BIG;

The BIG attribute makes the type have the size of CARDINAL.  The SMALL attribute (the default) stores the type in a byte type if possible.  Note, when the SMALL attribute is used and more than 256 enumeration identifiers are specified, the type will have the size of CARDINAL.

Using BIG can be useful when translating C language enumerations to Modula-2 since C enumerations are nothing more than integer constants.

Set type storage allocation adjustment.

The BIG attribute makes the set’s size the smallest possible size, which is a multiple of the size of CARDINAL.  The SMALL attribute (the default) stores the type in an 8-bit, or 16-bit type if possible.

When the SMALL attribute is used and the set contains between 1 and 8 elements, the type size will be 8-bits. When the SMALL attribute is used and the set contains between 9 and 16 elements, the type size will be 16-bits.  If the set has 17 or more elements, the set will be a multiple of size CARDINAL.

Record bit field support to allow easy interfacing with C language code.

To aid in interfacing with C language code, support for bit fields is added as an extension to the language. Bit fields are fields within a record that occupy an amount of space less than their declared type.

Bit fields can be declared anywhere in a record. Bit fields are declared within a bit field block which starts with the BITFIELDS keyword and ending with the END keyword.


        before : CARDINAL;

           bitField1 : CARDINAL BY 3;
           bitField2 : INTEGER BY 3;

        after : CARDINAL;

Signed types have a minimum bit size of 2, and unsigned types can occupy a single bit. Bit fields are grouped and packed via an algorithm compatible with nearly all C compilers. This makes translation of C structures quite straightforward.

Extensions to arrays types and variables

  • HIGH function extended to all arrays
  • ISO Modula-2 only allows you to pass array types to open array parameters. We also allow you to pass a variable whose type is the same as the element type of the open array parameter.
  • Anonymous array constants. Saves declaring and array type which is only used in a single constant declaration.
  • Open array constants. The compiler determines the HIGH bound of the constant by the number of array elements contained in the constant declaration.
  • Array slices like the Ada syntax.
  • Subscript checking suppression.The compiler will never perform array subscript checking at compile time or runtime for array types declared with the same lower and upper bound. For example 0..0. A single element array like this can be used for an array whose size is not known at compile time and is sized dynamically at runtime. This is only useful for single dimensional arrays. Dynamic arrays are a better alternative for arrays of indeterminate size.
  • Arrays of Char can be assigned to each other regardless of size.
  • An open array can be the destination or source of an assignment statement.

Type coercion

The coercion qualifier is specified by:


The coercion qualifier can be applied to any variable designator.  The data referred to by designator is treated as if it has the type specified by TypeName.  Coercion is a method of bypassing the strong type checking of Modula-2. Unlike ISO standard type casting type coercion has no restrictions on usage.

Use of type coercion is dangerous and not transportable, because it relies on knowledge of the data representation.  On the other hand, it is more efficient than other methods of bypassing type checking, such as use of ADDRESS types or variant records.  This is because no data is moved-you refer to the data in place.

The coercion qualifier can be used in some places where the CAST function, imported from the SYSTEM module, cannot be used such as the left hand side of an assignment statement.


number := buffer^[bp]:CARDINAL;
buffer^[bp]:CARDINAL := number

Type cast a literal constant

The compiler allows expression of the form. Example:


Taking the address of a literal constant

The compiler allows you to use the SYSTEM.ADR function on string literals and other structured constants. String literals are always null terminated.

CreateWindow(ADR("MyWindowClass"), ...);

Subscripting a string literal

The compiler allows you to subscript a named string literal constant.

Character literal constants

CHR(CompileTimeConstant) is treated as a character literal constant with the same privileges. For example the compiler treats CHR(13) the same as 15C. This means you can use CHR(13) with the string literal concatenation operator (+). This also applies to the ACHR, and UCHR functions.

CONST CrLf = CHR(13) + CHR(10); (* is accepted *)
CONST CrLf = 15C + 12C; (* ISO standard *)

Extensions to MIN and MAX

These pervasive functions can accept a parameter, which is a variable. The type of the variable is used for the return value of these functions.

Extension to SIZE

The SIZE pervasive function has been extended to accept open array and dynamic array variables.

The SIZE function allows you to reference record fields when it is passed a type parameter.

Function procedure call extension

Function procedures can be called as procedures. The function result value is discarded.

System type assignment compatibility

The parameter assignment rules of the types SYSTEM.BYTE, SYSTEM.WORD, SYSTEM.DWORD and SYSTEM.MACHINEWORD have been extended to all other assignment compatibility situations.

Parameter modes

The compiler supports marking VAR parameter types with more explicit modes of operation. INOUT and OUT identifiers are supported. INOUT parameters expect a value on input and return a  value upon exit. OUT parameters do not expect any input value and only output a value upon procedure return.

Using these help document code and helps, the compilers uninitialized variable detection.


The compiler supports marking value parameters as constant, CONST, parameters that cannot be altered within a procedure.

    constParam := 23; (* <== a compilation error *)
END p1;

Importing symbols

Importing all items from a module unqualified.

FROM ModuleIdentifier IMPORT * [EXCEPT Identifier {, Identifier}];

This has the effect of importing all exported symbols from a given module, except those identifiers in the exclusion list after the optional EXCEPT keyword. If an imported symbol would collide with an already visible symbol of the same name a compilation error is generated. In this case you would typically use the EXCEPT keyword to exclude the offending symbol from the import. Examples




EXPORTS declaration

Provides a mechanism to control which symbols are exported and visible outside of an executable file. This is normally only used for DLLs and shared objects. You can export individual symbols or entire modules.

Conditional Compilation

Conditional compilation is the compiler's ability to decide at compile time whether or not portions of your source program are to be compiled.  Conditional compilation can test a version tag that you define.

Version tags

You can use conditional compilation to create different versions of a program that reside in the same source.  The version that is compiled is controlled externally, by version tags.  

Version tags are identifiers you define in one of the following ways:

  • Through the development environment version tags option
  • Through the compiler version tag directive
  • On the compiler command line

Predefined compiler version tags

  • StonyBrook. This identifies our compiler. The Macintosh p1 compiler uses "p1" to identify itself.
  • Win32. The target operating system is Win32.
  • Linux. The target operating system is Linux.
  • SunOS. The target operating system is SunOS/Solaris.
  • Unix. The target operating system is any flavor of Unix. This tag is defined in addition to a specific operating system tag.
  • FlashTek. The target operating system is the X32 DOS extender.
  • Win16. The Target operating system is 16-bit Windows.
  • DOS. The target operating system is DOS.
  • ASCII. Modula-2 only. CHAR = ACHAR. 8-bit.
  • Unicode. Modula-2 only. CHAR = UCHAR. 16-bit
  • LittleEndian. The target processor uses little endian byte order.
  • BigEndian. The target processor uses big endian byte order.
  • IA32. The target processor is the IA-32 architecture. i.e. x86, Pentium, etc...
  • SPARC. The target processor is the SPARC processor.
  • Bits32. The target processor is 32-bit.
  • Bits16. The target processor is 16-bit.

The compile time IF statement

Conditional compilation is implemented by the compile time IF statement. Unlike the Modula-2 IF statement, the compile time IF operates at the token level.  A compile time IF can start between any two tokens in a program, and can control the compilation of any string of tokens.

The format of the compile time IF statement is:

%IF CompileTimeExpression %THEN
{%ELSIF CompileTimeExpression %THEN

CompileTimeExpression is an expression formed by version tags,  parentheses and the operators %AND, %OR, and %NOT.  The precedence of the operators is as follows, from highest to lowest:


TokenStream is any stream of Modula-2 tokens.

Compile time IF's can be nested up to 16 levels.  They can be used anywhere in a source file.

The CompileTimeExpressions are evaluated as boolean expressions.  The value of a version tag is TRUE if the version tag is defined and FALSE if it is not. The CompileTimeExpressions are evaluated in order until one is TRUE.  The TokenStream following the TRUE expression is compiled.  If none of the expressions is TRUE, and there is an ELSE, the TokenStream following the ELSE is compiled.  All other TokenStreams are skipped by the compiler.


A common use of conditional compilation is to add debug code that aids in debugging a program, but is not compiled in the final version of the program.  For example:

   WriteString('After ');
   WriteCard(i, 0);
   WriteString(' iterations: X = ');
   WriteReal(X, 10);
   WriteString(' Y = ');
   WriteReal(Y, 10);

Compile time IF's are not restricted to lists of statements, as in the above example.  You can also use them to conditionalize data structures:

TYPE SymbolRecord =
   DataType    : TypePointer;
   Address     : %IF ThirtyTwoBit %THEN
   NextSym     : SymbolPointer;

You can use the operators %AND, %OR, %NOT and parentheses to test more complex conditions. For Example:

%IF (Win32 %OR OS2) %AND %NOT Network %THEN

Alternate conditional compilation syntax in Modula-2

The compiler also accepts the conditional compilation syntax used by the Macintosh p1 compiler. The syntax is nearly identical except that it is contained in directives.

<*IF (Win32 OR OS2) AND NOT Network THEN *>

Selected listing of various SYSTEM module extensions

Machine processor count


This variable gives the number of installed processors in the computer.

Program exit code value


EXITCODE contains the value passed to the HALT procedure. Procedures installed into the termination chain may want to check the exit value of the program.

BuildNumber Variable

VAR BuildNumber : CARDINAL;

This variable contains the build number. This value is inserted by the linker via a linker option. The value is user defined.

DebuggerPresent Variable

VAR DebuggerPresent : BOOLEAN;

This variable is TRUE if the program is being debugged by the Stony Brook Debugger. Otherwise the value is FALSE.

OFFS Procedure

PROCEDURE OFFS(TypeOrVariableName.recordField{.recordField}) : ConstantValue;

OFFS returns a compile time constant value that is equal to the offset of the specified record field from the beginning of the record type.


   RecType =
        x, y, z : CARDINAL;

   yOffset = OFFS(RecType.y);

   v : RecType;
   v.x := OFFS(v.z);

Unreferenced parameters.


The compiler generates a warning when a parameter of a procedure is not referenced within that procedure.  In some instances it is necessary to have an unreferenced parameter within a procedure.  Operating system call back procedures are such an example. The UNREFERENCED_PARAMETER procedure can be used to suppress warnings on unreferenced parameters.


This identifier represents a string literal whose value is the filename of the source file being compiled.


This identifier represents a numeric constant whose value is the source line number where this instance of the identifier is used.

ASSERT Statement

This is a statement that checks a BOOLEAN expression for TRUE and if not raises an ASSERT exception. The assert exception will identify the module and line number of the failed ASSERT statement. ASSERT will generate this test code, if and only if, the version tag M2ASSERT is set, otherwise no code for the ASSERT statement is generated. The format of the ASSERT statement is:



Assume the M2ASSERT version tag is set.

   ASSERT(ptr <> NIL);

   (* will never get here if ptr = NIL *)
    (* think of the ASSERT statement like this
        IF ptr = NIL THEN
END foo;

ISASSERT function


This is a function that returns a BOOLEAN value. You can use this to trap ASSERT exceptions. It returns TRUE if the current exception is an ASSERT exception.



This is a function that returns the address where the current exception occurred. It will return NIL if there is no exception.


                         VAR OUT lineNumber : CARDINAL;
                         VAR OUT moduleName : ARRAY OF ACHAR);

This is a function that returns information about the current exception. The procedure will return NIL in addr if there is no exception. If the exception occurred because of a runtime check then the lineNumber and moduleName parameters will return the location of the checking error. If the exception did not result from a runtime check the lineNumber will return 0, and modulename will return an empty string.

SetUnhandledExceptionProc Procedure

PROCEDURE SetUnhandledExceptionProc(unhandled : PROC);

This procedure lets you set a procedure that will be called when an exception is unhandled.

AttachDebugger procedure (Win32 only)

     AttachDebuggerOpt =
     DoNotAttach,    (* do not attach, the default *)

     AttachExternal,(* attach on EXCEPTIONS.sysException only. this will primarily be access violations *)
     AttachAll (* attach on all raised exceptions. this includes ALL Modula-2 exceptions *)

PROCEDURE AttachDebugger(opt : AttachOptions);

Use this procedure to enable or disable just in time debugging support. You usually put this call as the first line of code in a program.

For example, if you use AttachExternal, then when you program gets an access violation or other system exception the debugger will be attached to the program allowing you to start debugging at the point of the exception.

OutputDebugMessage procedure

PROCEDURE OutputDebugMessage(str : ARRAY OF CHAR);

This procedure actually does nothing except that our debugger for Win32 and Unix systems will trap calls to this procedure and display the passed parameter in the debug messages window.

EnableCallTrace procedure

PROCEDURE EnableCallTrace;

By calling this procedure you enable the runtime system to perform a call trace when an exception of any kind is raised. If the exception goes unhandled then the call trace will be output to disk. If the exception is handled then the information is lost.

OutputCallTrace procedure

PROCEDURE OutputCallTrace;

This procedure is only used with EnableCallTrace. If you want to handle an exception, but still want the call trace output to disk then you should call this procedure inside your exception handler. For example this allows you to handle all exceptions, hopefully terminating gracefully, and still getting a call trace which may help you debug the problem.

TrapAccessViolations procedure (Unix systems only)

PROCEDURE TrapAccessViolations;

This procedure is used in shared objects to enable trapping access violations. Main programs automatically trap access violations. Access violations consist of the SIGSEGV, SIGBUS and SIGILL signals. These signals are global to an entire process, therefore our runtime system does not assume it can possess these signals when running in a shared object. Trapping an access violation means the violation is converted to a native language exception.

Note that it is still possible for code to override our runtime system signal handlers for these signals. This can happened in main programs and shared objects. When this occurs the violation will not be trapped by our runtime system.

VA_START, VA_ARG procedures

TypeIdentifier) : ADDRESS;

These procedures are used to support accessing the parameters of a procedure, which accepts a variable number of parameters (the VARIABLE procedure attribute). You use VA_START to get the address of the first variable argument. You then use VA_ARG for each argument passing the address VA_START initialized. The second parameter of the VA_ARG function is the type identifier of the argument you are about to read. The function returns a pointer to the data. The function also updates the parameter address, addr.

PROCEDURE example(format : ARRAY OF CHAR)
[RightToleft, Leaves, Variable];

    (*all pointers are the same size*)

    addr : ADDRESS;
    ptrCh : POINTER TO CHAR;

    ptrC := VA_ARG(addr, CARDINAL);
    (* use ptrC *)

    ptrCh := VA_ARG(addr, Pointer);
    (* use ptrCh *)
END example;

CLONE procedure

PROCEDURE CLONE(VAR newObject : <some_class_type>; sourceObject : <some_class_type>);

This procedure creates an exact copy of the object referenced by sourceObject and stores a reference to the new object in the variable denoted by newObject.

FUNC Keyword

This keyword allows you call a function procedure as a procedure, thus ignoring the result. No Warning will be generated in extended syntax mode, and no error generated in ISO mode.


   returnVal := MyFunction(param1, param2);
   FUNC MyFunction(param1, param2);

FIXME Procedure


This "procedure" allows you cause a warning to be generated in the source file at the source location with a string value passed to this function. No code is generated by the use of this procedure. This warning can never be suppressed with compiler options. You can use this feature to document something that needs addressing in your code and by having a warning generated you can use the mechanism's built into the development system for finding warnings errors in source files.


PROCEDURE SWAPENDIAN(IntegerType) : SameIntegerType;

This is a function procedure that allows converting a number between little and big endian byte ordering format. This only has use for code that stores data on disk, or transmits data across a network, that is used on computers that have different natural data formats. This procedure can be called as a function or a procedure. When call as a function only integer types can be used. When called as a procedure you can pass both integer types and floating point types.

16-bit, 32-bit and 64-bit INTEGER and CARDINAL types can be used with this function. REAL and LONGREAL can be used.


   card32 : CARDINAL32;
   card16 : CARDINAL16;

   r      : REAL;
   card32 := SWAPENDIAN(card32);
   card16 := SWAPENDIAN(card16);



PROCEDURE BIGENDIAN(IntegerType) : SameIntegerType;
PROCEDURE LITTLEENDIAN(IntegerType) : SameIntegerType;

These functions are like SWAPEENDIAN except they always result in a specific endian format. They assume the input data is in the native format for the target processor and operating system. Therefore these procedures may, or may not, generate code. For example LITTLEENDIAN will never generate code when targeting an IA-32 processor. It will however generate code if the target is a SPARC processor.

LITTLEENDIAN is equivalent to

%IF BigEndian %THEN

Multi-processing support functions

The compiler implements various intrinsic functions useful in multiprocessing. These functions generate inline machine code rather than a procedure call. These functions perform atomic operations in a multiprocessing environment. This means that the processor executing these functions has exclusive access to the data being operated on.

Atomic compare and exchange

PROCEDURE ATOMIC_CMPXCHG(VAR INOUT data : SomeType; compare, source : SomeType) : SomeType;

where SomeType can be CARDINAL, INTEGER, DWORD or ADDRESS. Remember that pointers are compatible with ADDRESS parameter types.

This procedure compares the value in data with the value in compare and if equal, then the value in source is assigned to data. The function result is the value previously held in data. Only the variable data is atomically accessed. This function makes the new value written in data visible to other processors before returning.

Atomic exchange

PROCEDURE ATOMIC_XCHG(VAR INOUT data : SomeType; source : SomeType) : SomeType;

where SomeType can be CARDINAL, INTEGER, DWORD or ADDRESS. Remember that pointers are compatible with ADDRESS parameter types.

This procedure performs an atomic exchange of the value in data with the value in source. The function result is the value previously held in data. Only the variable data is atomically accessed. This function makes the new value written in data visible to other processors before returning. This operation may be used as a function or as a statement ignoring the return value.

Note: The return value may not be the same as the value in data, because another processor may have altered the value one processor cycle after this function alters the value.

Atomic add

PROCEDURE ATOMIC_ADD(VAR INOUT data : SomeType; constantValue : INTEGER) : SomeType;

where SomeType can be CARDINAL or INTEGER.

constantValue must be in the range

-128..127 for IA32
-4096..4095 for SPARC

This procedure adds the value in constantValue with the value in data. Positive numbers perform addition and negative numbers perform subtraction. The function result is the result of the addition/subtraction to data. Only the variable data is atomically accessed. This function makes the new value written in data visible to other processors before returning. This operation may be used as a function or as a statement ignoring the return value.

Memory fence/barrier


Note: This procedure is not necessary and does not perform any actions on IA-32 architecture processors (x86 processor family) since these processors support strong write ordering.

Note: You do not need to use a memory fence with any synchronization objects supported by the runtime library or the operating system, as they will take necessary actions.

This procedure generates a memory fence or barrier, and is necessary on processors that do not guarantee memory access order.

  • PowerPC
  • IA-64

The procedure guarantees that all subsequent loads or stores will not access memory until after all previous loads and stores have accessed memory, as observed by other processors. The following pseudo code describes this further

1) <Acquire lock>
3) critical section code
5) <release lock>

The first memory fence stops the processor from looking ahead and pre-fetching any data used in the critical section. The second memory fence makes sure that any data written in the critical section is made visible to other processors before the write that releases the software lock. The memory fence is generally only used when implementing spinlocks.

Von Mensch zu Mensch
Home · Modula-2

 Copyright © TERRA Datentechnik 1996-2006
Alle Rechte vorbehalten.TERRA Datentechnik übernimmt keine Haftung für die Vollständigkeit, Richtigkeit und Aktualität der Angaben!
Rechtliche Hinweise  ·  Firmenprofil
Contact us   Last Updated: 12-November-2002 Webmaster