This page briefly goes over various language extensions we have added.

Additional predefined types

CARDINAL8, CARDINAL16, CARDINAL32, CARDINAL64, SHORTCARD, LONGCARD, INTEGER8, INTEGER16, INTEGER32, INTEGER64, SHORTINT, LONGINT,  ACHAR, UCHAR, BYTEBOOL, BOOL8, WORDBOOL, BOOL16, DWORDBOOL, BOOL32

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.

NILPROC

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

Unicode support

Bitwise Operators

BAND, BOR, BXOR, BNOT, SHL, SHR, SAR, ROL, ROR

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:

POINTER TO ARRAY OF {ARRAY OF}TypeName

Example:

TYPE
   Matrix = POINTER TO ARRAY OF ARRAY OF REAL;

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

Support for assembly code

The compiler contains a built-in assembly language parser.

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.

Example:

DEFINITION MODULE MyModule;

PROCEDURE it;

PROCEDURE add(a, b : CARDINAL) : CARDINAL; MACRO;
BEGIN
    RETURN a + b;
END add;

PROCEDURE it2;

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

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

Examples:

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

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

GtkPathPriorityType =
(
GTK_PATH_PRIO_LOWEST= 0,
GTK_PATH_PRIO_GTK= 4,
GTK_PATH_PRIO_APPLICATION = 8,
GTK_PATH_PRIO_RC= 12,
GTK_PATH_PRIO_HIGHEST= 15,
GTK_PATH_PRIO_MASK= 00fh
);

Enumeration type storage allocation adjustment.

Example:

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.

Example:

   TYPE BITREC =
        RECORD
        before : CARDINAL;

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

        after : CARDINAL;
        END;

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

Type coercion

The coercion qualifier is specified by:

designator:TypeName

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.

Example:

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

Type cast a literal constant

The compiler allows expression of the form. Example:

SYSTEM.CAST(CARDINAL, -8)

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.

Example:
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.

PROCEDURE p1(VAR INOUT param1 : INTEGER; VAR OUT param2 : INTEGER);

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

PROCEDURE p1(CONST constParam : INTEGER);
BEGIN
    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

FROM SYSTEM IMPORT
   DWORD;

FROM WIN32 IMPORT * EXCEPT DWORD;

FROM WINUSER IMPORT *;

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:

Predefined compiler version tags

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
   TokenStream
{%ELSIF CompileTimeExpression %THEN
   TokenStream}
[%ELSE
   TokenStream]
%END

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:

%NOT
%AND
%OR

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.

Examples:

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:

%IF DEBUG %THEN
   WriteString('After ');
   WriteCard(i, 0);
   WriteString(' iterations: X = ');
   WriteReal(X, 10);
   WriteString(' Y = ');
   WriteReal(Y, 10);
%END

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 =
   RECORD
   DataType    : TypePointer;
   Address     : %IF ThirtyTwoBit %THEN
                      CARDINAL;
                 %ELSE
                      LONGCARD;
                 %END
   NextSym     : SymbolPointer;
   END;

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
...
%END

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 *>
...
<*END*>

Selected listing of various SYSTEM module extensions

Machine processor count

VAR CPUCOUNT : CARDINAL;

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

Program exit code value

VAR EXITCODE : CARDINAL;

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.

Example:

TYPE
   RecType =
    RECORD
        x, y, z : CARDINAL;
    END;

CONST
   yOffset = OFFS(RecType.y);

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

Unreferenced parameters.

PROCEDURE UNREFERENCED_PARAMETER(AnyParameter);

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.

SOURCEFILE Variable

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

SOURCELINE Variable

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:

ASSERT(BooleanExpression);

Example:

Assume the M2ASSERT version tag is set.

PROCEDURE foo(ptr : ADDRESS);
BEGIN
   ASSERT(ptr <> NIL);

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

ISASSERT function

PROCEDURE ISASSERT() : BOOLEAN;

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.

EXCEPTADR Procedure

PROCEDURE EXCEPTADR() : ADDRESS;

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

EXCEPT_INFO Procedure

PROCEDURE EXCEPT_INFOADR(VAR OUT addr : ADDRESS;
                         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)

TYPE
     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

PROCEDURE VA_START(VAR OUT addr : ADDRESS);
PROCEDURE VA_ARG(VAR INOUT addr : ADDRESS;
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];

TYPE
    Pointer = POINTER TO CARDINAL;
    (*all pointers are the same size*)

VAR
    addr : ADDRESS;
    ptrC : POINTER TO CARDINAL;
    ptrCh : POINTER TO CHAR;

BEGIN
    VA_START(addr);
    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.

Example:

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

FIXME Procedure

PROCEDURE FIXME(str : ARRAY OF CHAR);

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.

SWAPENDIAN Procedure

PROCEDURE SWAPENDIAN(IntegerType) : SameIntegerType;
PROCEDURE SWAPENDIAN(VAR INOUT : IntegerOrRealType);

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.

Examples:

VAR
   card32 : CARDINAL32;
   card16 : CARDINAL16;
 

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

   SWAPENDIAN(card32);
   SWAPENDIAN(r);

BIGENDIAN, LITTLEENDIAN Procedures

PROCEDURE BIGENDIAN(IntegerType) : SameIntegerType;
PROCEDURE BIGENDIAN(VAR INOUT : IntegerOrRealType);
PROCEDURE LITTLEENDIAN(IntegerType) : SameIntegerType;
PROCEDURE LITTLEENDIAN(VAR INOUT : IntegerOrRealType);

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
   SWAPENDIAN(data);
%END

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

PROCEDURE MEMORY_FENCE;

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.

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>
2) MEMORY_FENCE
3) critical section code
4) MEMORY_FENCE
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.