MIRA SOFTWARE MODULA-2 COMPILER For the ZX Spectrum (c) Mira Software 1991 0:Contents ................................................. 1 1:Using the editor and compiler ............................ 2 2:A short Modula-2 tutorial ................................ 6 Introduction ......................................... 6 Assignment and data types ............................ 8 The libraries of procedures .......................... 10 Control structures ................................... 11 Writing your own procedures .......................... 14 Planning your programs ............................... 16 Arrays and subrange types ............................ 17 Records and the WITH statement ....................... 19 Enumerated types and variant records ................. 20 Set Types ............................................ 21 Pointer types and recursion .......................... 22 Modules and procedure types .......................... 23 Concurrent Programming ............................... 25 3:Summary of the implementation ............................ 28 Compiler Directives and how the compiler works ....... 30 4:Error Messages ........................................... 32 5:The library of modules ................................... 34 STDIO - Standard input and output .................... 35 DRIVES - Loading, Saving, etc. ....................... 37 FASTO - Fast screen output ........................... 39 MAINREAD - Input from upper screen ................... 39 STRINGS - Character string manipulation .............. 41 PLUS3IO - Files on the +3 ............................ 42 DATA - Searching and sorting ......................... 43 HARDWARE ............................................. 44 BASIC ................................................ 45 ERRORS ............................................... 46 SETOPS - Set operators for large sets ................ 46 STORAGE - Allocation for pointer variables ........... 47 1 GRAPH0 ............................................... 48 GRAPH1 - Line drawing ................................ 48 GRAPH2 - Circles and ellipses ........................ 49 GRAPH3 - Arcs ........................................ 49 CLIPPING ............................................. 50 FILL - Contour filling ............................... 50 SYMBOL - Sprite-like symbols ......................... 51 NUMIO - Simple numeric input & output ................ 52 R2IO \ ............................................... 53 R3IO | - Input & output for real numbers ............. 53 R5IO / ............................................... 53 R2MATH \ ............................................. 54 R3MATH | - Mathematical functions .................... 54 R5MATH / ............................................. 55 6:General information ...................................... 56 1:USING THE COMPILER To start using the compiler you should first follow the instructions on the Installation sheet, to load the compiler and to transfer any modules which are required from tape to drive. You will arrive at the request "Enter name of program". If you want to load a module at this stage then enter its name, otherwise press the down key (CAPS SHIFT & 6). The message "STOP in INPUT" will appear and the compiler will be in control key mode; i.e. pressing keys will perform given actions. Entering Lines Pressing ENTER will put the compiler into line entry mode which allows you to type in the program as a series of lines. The program is listed on the screen as the lines are entered, with a cursor (flashing '>') marking the current line. Note that Modula-2 is not a line based language so that ends of lines do not necessarily separate statements. Lines consisting of no characters will not be entered, so to produce a blank line you should enter a line consisting of a space. In order to leave line entry mode and go into control key mode you should press the down key. CONTROL KEYS The cursor may be moved up or down a line by pressing the up or down keys (CAPS SHIFT & 7 or CAPS SHIFT & 6). Pressing 7 or 6 will move the cursor up or down eight lines at a time. SYMBOL 2 SHIFT & 7 will move the cursor to the start of the program and SYMBOL SHIFT & 6 will move it to the end. The current line may be edited by pressing 1 or EDIT. Pressing E allows the entry of a line before the current line. (Entering line entry mode by pressing ENTER will add lines after the current line). Pressing H gives a list of control keys and their functions on the screen. Moving Blocks of lines It is possible to move a block of lines within the program. To do this you should move the cursor to the start of the block and press B to mark it, then move the cursor to the end of the block, and press K to store the block (Cut). The block may be later retrieved at the position of the cursor by pressing P (Paste). Pressing P again will then produce further copies of the block that has been stored. Note that what is in the paste buffer may be erased by some disc drive operations. A block of lines may be deleted by marking it as above and pressing CAPS SHIFT & 0 (DELETE). Pressing V will list the marked block on stream 3 (i.e. a printer). If no block is marked then the whole program will be printed. Deleting the whole program Note that the whole program may be deleted by pressing SYMBOL SHIFT & 7, B, SYMBOL SHIFT & 6, then CAPS SHIFT & 0. The Options byte Pressing the letter O allows entry of a number which is the options byte. This specifies what is to be used for storage and loading of programs. 0 represents tape storage, 13 represents the ramdisc, and numbers between these give the corresponding drive. 15 specifies that loading will first try the ramdisc, then drive 1, and saving will be to both drive 1 and ramdisc. In the tape-only versions drive 1 refers to the tape but with messages sent to the printer indicating what action is required. Adding 16 to the number specifies that a line feed ( CHR$(10) ) is to be added to the end of lines when saved in a file or printed out. This will allow microdrive files to be edited by word processors such as Tasword 3. (When a file is loaded into the compiler any line feeds are removed). Adding 32 to the number sets the compiler to search for errors in the program rather than produce any code. (See Section 4). Adding 64 instructs the compiler to save the compiled code and symbol table at the end of compilation. The code is saved with the name of the program preceded by a '#', to the drive specified by the options byte. (See section 3 for further details). 3 Saving and loading programs To save a program you should press S and give the name of the program as required. Just pressing ENTER will use the name given in the module heading or COPY SOURCE if no name is found (ª SOURCE for Wafadrive). If the program already exists on the drive then you will be asked for confirmation that it should be overwritten. To load a program press L and give the name of the program (pressing ENTER will give a catalogue of the drive, or load the next program if tape is used). If this is done with a program already present then the new program will be merged into the old one at the position of the cursor. If you do not want the programs to be merged then press SYMBOL SHIFT & L instead. This will cause the old program to be saved and then removed from memory before the new one is loaded. (Note 1: The old program is only saved if it has been altered since it was loaded. It is saved to ramdisc if available, otherwise to drive 1 - this is irrespective of the options byte. It is not saved in the tape only version.) Programs are stored in the following ways: Ramdisc ...... Basic-type program Disciple ...... Code Tape ......... Basic-type program Microdrive .... File Beta ......... Basic-type program Opus .......... File Wafadrive .... Basic-type program Plus 3 ........ Code Exiting from the compiler Pressing STOP (SYMBOL SHIFT & A) will save the current program as in Note 1, then perform a NEW command, allowing a return to Basic. The compiler will still be in memory, and can be re-entered at the address given on the installation sheet. Finding words Pressing F allows you to enter a word to be found in the program. This always starts looking from the beginning of the program. Pressing N searches for the next occurrence of the word. If the word is found then it is marked with a flashing '?'. Compiling programs When a program has been entered into the compiler it may be compiled by pressing the X key. Other modules will be loaded from ramdisc or drive 1 when required. (The current program will be 4 saved as in Note 1.) The compilation takes place in several passes of the program, and sometimes the same module will need to be loaded more than once. If a mistake in the Modula-2 program is detected during the compilation an error message will be displayed with a flashing '?' at the position in the program where the mistake was detected, and the cursor at the line containing it, ready for the line to be edited and the mistake corrected. While the compilation is in progress the table of identifier names etc. will be stored in screen memory, resulting in a random pattern appearing on the screen. (This is why messages have to be sent to the printer in the tape-only version.) When the compilation has been successfully completed the words CLEAR and SAVE followed by numbers will be printed on the screen. Pressing any key will perform a NEW command, with the compiled code stored above RAMTOP. The compiler will still be in memory, and can be re-entered using the entry point given on the installation sheet. The compiled program may be run by using RANDOMIZE USR 48500. This may be within a Basic program if required. If an error occurs in the running of the Modula-2 program then an error message will be displayed. If the error occured in a ROM routine then this will cause an exit from the program and a normal Spectrum error message will be given with the number of the Basic line which called the Modula-2 code. If it is in the Modula-2 run-time routines, then an appropriate error message is given, and the user will be given the option of continuing the program. However, doing so may well cause a crash, so you are recommended to exit at this point, when the error message "BREAK into program" will be given. The position of the error in the Modula-2 code can be found by returning to the compiler, selecting option 32 and pressing X. The start of the compiled code is given by the number following SAVE, and the end is at 49152. This is the code which will be saved using option 64. On reloading you should first set RAMTOP by clearing to the number following CLEAR (less if dynamic variables are used, as described later.) 5 2:A short Modula-2 tutorial This section is meant to assist you in learning how to program using the compiler. Some previous experience of programming would be useful. It is not designed as a programming course, for which you should choose a book to your own taste. The modules Terminal, InOut, RealInOut, MathLib0, LineDrawing and Processes are included with this compiler for compatibility with textbooks on Modula-2. However, it is recommended that you use the modules described in section 5 instead as soon as you understand how to do so. Note that very few examples are given here, and it is recommended that when a keyword is given in the text, you should find it in the example programs (using the F key), to see how it is used. The example programs are CLOCK, ROUTE, STARBASE, and SYMDES, with subsidiary modules VILLAGES and RADECSTG. You will be able to load all of these into the ramdisc if you have a Spectrum with 128K. Introduction If you have programmed in Basic you will realise that numbers and character strings are two different types of data, and operations which apply to one do not apply to the other; e.g. you cannot add a number to a string. In other languages such as Pascal this idea of having different types of data is taken further with numbers being separated into the INTEGER type of whole numbers, and the REAL type of numbers which may have a decimal part. The CHAR type consists of characters, and the BOOLEAN type has the values TRUE and FALSE. Modula-2 also has the CARDINAL type of positive whole numbers, and several other types defined. As well as these you will see later that you can define new data types. All this means that it is vital for the programmer to be aware of the type of every item of data used in a program. This strictness of the language makes it harder to learn than a language such as Basic, but means that programs can be made more reliable and efficient. Note that unlike many other languages, in Modula-2 capital and lower case letters are considered distinct, so that when a variable or other identifier is used, it has to match its declaration exactly. All of the Modula-2 keywords have to be in capitals. In this implementation the names of procedures from the library modules are also in capitals. The following is an example program for you to consider. 6 MODULE addup; VAR a,b,sum:CARDINAL BEGIN WRITELN('Enter first number'); READLN(a); WRITELN('Enter second number'); READLN(b); sum:=a+b; WRITELN('The sum is',sum) END addup. From this you can see how a Modula-2 program is put together. It starts with the word MODULE followed by the name of the program and a semicolon. Then come sections declaring what is going to be used in the program, each ending with a semicolon. In this case there is only one such section, starting with VAR indicating that variables are being declared, their names being a, b and sum and they are all of type CARDINAL. BEGIN indicates that the declarations are finished and this marks the start of the statements of what is going to be done by the program. These statements are separated by semicolons. The end of the program is indicated by the word END followed by the name of the module and a full stop. Note that END is not a statement (it does not indicate any action to be performed) and so does not need a semicolon to separate it from the statement before it. You will probably know that a variable is a place to store data, but the type CARDINAL may be unfamiliar to you. This type is composed of the positive whole numbers. Thus the variable declaration indicates that the variables a, b and sum may only store such values. The procedure WRITELN prints items on a line on the screen. The quote marks indicate a string of text, so that whatever is inside them is printed out as written. The second item in the last WRITELN statement is not in quotes, and so is not printed out as the word sum, but as the value stored in the variable called sum. The procedure READLN gets a value typed in at the keyboard, and puts it in in the specified variable. The statement sum:=a+b adds together the values in the variables a and b and puts the result in the variable sum . So the given program prompts the user to enter two values at the keyboard, then adds them together and prints the result. Now that the working of this program has been described, you 7 should type it in for yourself, but without looking at it in the manual. Try to do this by remembering how the program works, rather than by memorizing it. When you have typed it in, make sure that you can compile and run it without any errors. Thus the form of a Modula-2 program is as follows; MODULE mname; declarations; BEGIN statements; END mname. (Note: Here, and in following definitions of parts of Modula-2 programs, words in BOLD text are Modula-2 keywords; i.e. they are actually typed in the program. Other words are descriptions of what occurs in a given position.) The program starts running after the BEGIN keyword, and executes the statements in the order given - fairly obvious but surprisingly often forgotten. When you are writing a program it is useful for you to be able to imagine that you are the computer, and work through what the program does by hand. Assignment, simple data types and operators The statement sum:=a+b in the previous example is called an assignment statement. In general this will take the form: variable:=expression and will work out the expression on the right of the := and put it into the variable on the left. The expression and the variable should be of the same type, or else their types should be compatible. Rules for when two different types are compatible are given at the appropriate places in this manual. Note that any variable used in a Modula-2 program must be declared following the VAR keyword, in the declarations part of the program. VAR variable1, variable2, ...:type; Variable names must start with a letter, which may be followed by further letters and digits. Spaces and symbols may not be part of variable names. We now take a closer look at some of the data types of Modula-2 and the operations which apply to them. INTEGER, includes positive and negative whole numbers (within the limits -32768 to 32767); e.g. 10, -1234, 30000. The operators +, -, *, DIV and MOD, apply to INTEGER numbers. You probably know that * represents multiplication. For two integers p 8 and q, p DIV q is the integer part of p divided by q, and p MOD q is the remainder. CARDINAL includes positive whole numbers in the range 0 to 65535; e.g. 23, 49152. CARDINAL constants may also be written as hexadecimal numbers followed by H or as octal numbers followed by B; e.g. 36B, 0C000H. All numbers must start with a digit, so a leading zero may be needed for the hexadecimal representation. The operators +, -, *, DIV and MOD apply to CARDINAL numbers. REAL includes numbers which may have a decimal part. REAL constants put in a program must have a decimal point and at least one digit on each side of it. They may also have the letter E followed by a number, indicating multiplication by a power of 10; e.g. -3.14159, 1.23E6, -2.0E-7. REAL numbers are stored in 5 bytes with a 1 byte exponent and 4 byte mantissa. The operations on them are those from the Spectrum ROM. This compiler also implements numbers with a 1 byte exponent and 1 and 2 byte mantissae called REAL2 and REAL3 respectively. These are not as accurate as REAL numbers, but give much quicker calculations. Note that REAL may also be called REAL5. The operators +, -, *, / apply to numbers of all three real types (/ represents division). Different numeric types may not be mixed in an expression. For instance, if n is an INTEGER variable and x and y are REAL variables then the statement y:=n*x is not allowed. Functions are provided to convert numbers to a given numeric type from any other numeric type. These are called INT, CARD, RL5, RL3 and RL2 respectively. Thus the above statement should be y:=RL5(n)*x. These functions are the only way to put constants of type REAL2 or REAL3 into a program; e.g. RL3(3.1416), RL2(60). The type CHAR includes letters and the rest of the Spectrum character set. (Note that variables of type CHAR can only store a single character. You will see how to deal with strings of characters later). Constants of type CHAR consist of a character in single or double quotes. They may also be given as the ASCII code of the character in octal followed by the letter C or in hexadecimal followed by the letter I. The function CHR is used to convert a CARDINAL expression into the corresponding character in the ASCII sequence. The function ORD does the reverse, giving the ASCII code, of type CARDINAL, for a value of type CHAR. The following expressions all represent the same value of type CHAR: 'A', "A", CHR(65), 41I, 101C. Expressions of the above types may be compared using the relational operators, which are: <, >, <=, >=, =, <> . The result 9 of these comparisons is a value of type BOOLEAN. This type has just two values, TRUE and FALSE. BOOLEAN expressions find their main use in control structures, such as IF statements, which we shall come to later. However, just as with any other type, it is possible to declare variables of type BOOLEAN, and assign BOOLEAN expressions to them. The operators whcih apply to BOOLEAN expressions are AND, OR and NOT. Usually the correct usage of these operators is obvious from what would be said in English, but sometimes some thought is needed. Whereas in English you might say "If the variable x is not equal to 1 or 2", the appropriate Modula-2 expression would not be x <> (1 OR 2), nor (x<>1) OR (x<>2). It should actually be (x<>1) AND (x<>2). The expression NOT((x=1) OR (x=2)) would also be correct. The types CARDINAL, INTEGER, CHAR and BOOLEAN are known as ordinal types. An ordinal type consists of a sequence of values, so that given a value of that type it is possible to say what the following one is (unless the value is the last in the sequence). For example 3 is followed by 4, 'K' is followed by 'L'. The procedures INC and DEC apply to ordinal variables to give the next or previous value. For instance, if x is a CARDINAL variable then INC(x) does the same as x:=x+1. The functions MAX and MIN give the maximum and minimum values of an ordinal type; e.g. MAX(CHAR) is CHR(255). Note that the real types are not ordinal types, since, for instance, it is not possible to say what the next REAL number after 1.6 is. It is possible to give names to constants, using a CONST declaration, in the declarations part of the program; e.g. CONST pi=3.1415; SIXTY=RL3(60); maxcount=100; Giving constants names is useful for making your programs more readable and easier to maintain. It may also mean that less memory is used, as the value only needs to be stored once. The libraries of procedures In the above example you saw the use of the built-in procedures READLN and WRITELN. In fact this involved a bit of cheating as these procedures do not really belong to Modula-2, but are simplified forms of Pascal procedures, and are implemented in this compiler to give you a simple method of input and output. The idea of Modula-2 is not to have many procedures built into the compiler itself, but to be able to get them from a library of separate 10 modules. The installation sheet tells you how to transfer these modules from tape to a drive. To be able to use a procedure from a library module, this must be declared in an import list as follows: FROM module IMPORT procedure1,procedure2 ...; (Note: The 3 dots ... mean that a list of items may appear in your program. This is pointed out to distinguish it from the token consisting of 2 dots .. which is part of the Modula-2 language.) Import lists should be positioned in your program after the module heading, and before any declarations. A procedure is called simply by putting its name in your program. Most procedures require parameters, that is items of data to act upon. These should be given in brackets after the procedure name. For example, the procedure SETATTRS from the module GRAPH0 is used to set a number of the attribute squares on the screen to a given attribute. This requires the x and y position of the first attribute square to be set, the number of squares to be set, and the attribute they are to be set to. The parameters must be in the order specified. So SETATTRS(0,10,128,32) would set the middle 4 lines of the screen to be green paper with black ink. Somewhere else in your program you might want to clear the graphics screen using the procedure GCLS, from the same module. This is a procedure which does not require parameters, so the statement calling it would just be the word GCLS, without brackets following it. Note that to use these two procedures in a program the import list: FROM GRAPH0 IMPORT SETATTRS,GCLS; would have to appear at the start of your program. A function is a kind of procedure which is not used as a statement on its own, but to return a value as part of an expression. Examples of functions are SIN and COS from the module R5MATH. The procedures available from each module, and the number and types of parameters which each requires, can be seen from the definition part of the module. These are given in section 5 of this manual. Further details of what these procedure headings mean are given in the section on writing your own procedures. The module SYSTEM is built into the compiler, rather than being loaded in, and deals with certain system-dependent facilities. Control structures The first control structure we deal with will be familiar to Basic programmers, it is the IF statement. IF condition THEN statements END Here the statements following THEN are only executed if the condition is TRUE. The condition has to be a BOOLEAN expression. 11 The IF statement is written on one line here, but usually the statements in the IF statement would be written on separate lines. Remember though that it is not putting statements on separate lines which separates them, but putting semicolons in between them. These statements may be assignments, procedure calls, but will often be other control structures. In particular IF statements may be nested, one inside another. IF statements may also contain ELSE. IF condition THEN Statements ELSE Statements END If the condition is true then the statements following the THEN are executed, if not then the statements following the ELSE are executed. Note that when IF statements are nested, the fact that each has to have an END means that there is no ambiguity about which one an ELSE may belong to. Sometimes it is useful to be able to select from several different alternatives. To avoid the need to have more than one IF statement, ELSIF can be used. IF condition THEN Statements ELSIF condition THEN Statements ELSIF condition THEN Statements . . ELSE Statements END The program goes through the conditions until one is found which is TRUE, then the corresponding statements are executed. None of the other statements are executed, even if later conditions are also TRUE. If all the conditions are FALSE then the statements following the ELSE are executed (if the ELSE part is present). Often the selection of different alternatives depends upon the value of an ordinal expression. Instead of having an IF & ELSIF statement with the same expression in each comparison the CASE statement is available. CASE expression OF const1:Statements| const2:Statements| . . constn:Statements ELSE Statements END 12 (The | character which separates the cases is obtained by shifted S in extended mode on the Spectrum.) Here the statements are executed which follow the constant equal to the value of the expression. If none of them are then the statements following the ELSE are executed (if present). Instead of just a single constant at the start, a case may have a range consta..constb or a list of constants (or ranges) separated by commas. The corresponding statements will be executed if the expression is equal to any of the constants in the list or within the range. The above statements deal with choosing between several alternative actions. Another important aspect of programming is repeating actions. In Modula-2 the statements concerned with this are the FOR, WHILE, REPEAT and LOOP statements. The FOR statement will be familiar to Basic programmers, but there is no NEXT statement, the end of the repeated part being marked by END. FOR variable:=expression1 TO expression2 DO Statements END The first expression will be assigned to the variable and then the statements executed once for each value of up to the second expression. If the second expression is less than the first then the statements will not be executed. The types of the expressions must be compatible with that of the variable, which must be an ordinal type. The FOR statement may have a constant step value after the word BY. FOR variable:=expression1 TO expression2 BY ConstExpr DO Statements END The REPEAT loop is as follows: REPEAT Statements UNTIL condition This should be fairly clear; the statements are repeatedly executed until the condition is satisfied. Note that the condition is checked at the end of the statements so they will be executed at least once. Alternatively, using the WHILE statement, the condition is checked at the start. WHILE condition DO Statements END Here the condition is checked and if it is satisfied then the statements are executed. This is repeated until the condition is not satisfied. Note that if the condition in a REPEAT loop becomes satisfied in one of the statements, the rest of the statements will still be executed before it is checked, and similarly with the WHILE loop. To be able to exit from a loop from within the statements, a more 13 general construct is required. This is the LOOP statement LOOP Statements END As it stands this will be an infinite loop, and the statements will be repeated indefinitely. The programmer must put in explicitly where an exit from the loop is required, using an EXIT statement which is just the word EXIT. Usually this will be in an IF statement, allowing conditional exit from the loop. You have now seen the assignment statement, procedure calls, and the IF, CASE, FOR, REPEAT, WHILE, LOOP and EXIT statements. These are all of the statements of Modula-2 except for two (the WITH and RETURN statements). Note that there is no GOTO statement in Modula-2. A backwards jumping GOTO means some sort of loop, and a forward GOTO is either used to jump out of a loop or to miss out statements depending on a given condition. Modula-2 has control structures to deal with each of these cases so GOTO is not required. Writing your own procedures You have seen how to call procedures from the library modules; now you will see how to write such procedures in your own programs. The form of a procedure definition is much the same as that of the whole program. PROCEDURE name; declarations BEGIN Statements END; The procedure definition goes in the declaration section of the program. The procedure is called simply by putting its name as a statement in your program. The declaration section of the procedure may contain VAR and CONST declarations. These are then local to the procedure; i.e. the variables cannot be referred to outside the procedure, although the procedure may refer to variables in the main program. In the same way the procedure may contain other procedure declarations. A procedure call is like a GOSUB in Basic, since the position in the program when is remembered, for resumption when the procedure ends. Do not make the mistake of thinking of a procedure call as a GOTO the procedure start. To be able to pass parameters to a procedure, they have to be declared in brackets directly after the procedure name. PROCEDURE name(param,...:typename;...); 14 The parameters declared here are called formal parameters and within the procedure they behave as variables containing the values passed to the procedure. To pass these values from the program when the procedure is called they must be put in brackets after the procedure name. name(expression...); These values are called actual parameters. Note that for each formal parameter, there has to be a corresponding actual parameter of compatible type. The actual parameters may be constants, variables or an expression which will be worked out to find the value passed. This type of parameter is called a value parameter, as the only information passed to the procedure is the value of the expression. There is another sort of parameter where the actual parameter must be a variable. This is called a variable parameter and is indicated by putting the keyword VAR before the formal parameter. PROCEDURE name(VAR param,...: typename;...); Within the procedure the formal parameter represents the variable passed as the actual parameter. In particular if the formal parameter has a value assigned to it, then it will actually be assigned to the actual parameter. This means that variable parameters may be used to return values from a procedure. The type of the actual parameter has to be the same as that of the formal parameter rather than just compatible (with a few exceptions). Be careful to distinguish between the VAR occurring here, and that in variable declarations. If a formal parameter has type WORD then the actual parameter may be of any type that requires one byte of storage space. The type WORD must be imported from the SYSTEM module, and represents a single byte of storage. In order to manipulate an item of type WORD it may be necessary to convert it to another type by means of a typecast, which interprets an expression of one type as having another type. A typecast takes the form typename(expression). The expression must require the same number of bytes of storage as the type it is converted to. For a variable v of a type T, the number of bytes of storage needed is given by SIZE(v), or by TSIZE(T). The function TSIZE must be imported from the SYSTEM module. Although variable parameters provide a way of returning values from procedures, another method, when only one value is to be returned, is to use a function procedure. This returns a value rather than being a statement in its own right. This may make your 15 program neater; for instance, it is possible to write WRITELN(SIN(x)) whereas with a variable parameter it would need to be something like SINCALC(x,y);WRITELN(y). The heading of a function is as follows: PROCEDURE name(param,...:typename1...):typename2; Here typename2 is the type of the parameter returned by the function. Within the statements making up the body of the function there must be a return statement, which takes the form: RETURN expression The value of the expression is then returned by the function. Procedures which are not functions may contain the RETURN statement, without an expression following, to give a return to the calling program before the end of the procedure. Functions are usually thought of in a mathematical context, and so have a numeric type, but they may have other ordinal types, such as BOOLEAN. If you look at the procedures for inputting numbers from the module NUMIO, you will see that they are defined as functions without parameters, so instead of putting RDCARD(x) you need to put x:=RDCARD(). This means that the type of x only needs to be compatible with CARDINAL. Note that functions without parameters still require the brackets to be present, whereas normal procedures without parameters would not have brackets. Planning your programs Procedures are a very important part of good programming practice. They allow you to divide your program up into parts which do not interact with each other except in a well defined way. Then you only have to deal with one part of your program at a time. Giving your programs this sort of heirarchical structure is part of what is called structured programming. There remains the question of the order in which this hierarchy is put together. One way, called top-down programming, is to write the main program first, putting in procedure calls without having written the procedures. This means that when you come to write the procedures you have a clear idea of what is required. Alternatively, you can write procedures first, and later put them together to form a program. This is known as bottom-up programming, and has the advantage that the procedures can be written so as to be useable in other programs as well. Note that the name bottom-up programming has been mistakenly given to unstructured programming by some proponents of top-down programming. Which you 16 actually choose depends upon the nature of what you are doing. Bottom-up programming is suited to trying out new ideas, when the final form of the program is not too critical. Top-down programming is suited to producing programs intended for long-term use. Another important aspect of programming is planning what you are going to do in advance, before you start typing the program in. This is often a problem, as programmers are usually eager to start typing, and there is little point in simply writing out the whole program first on paper. Two items of advice may be useful. One is to plan on paper what structure the data in the program is going to take. The coding will then take the form of writing procedures to deal with this data. The other is to plan how the inputs and outputs of the program will appear on the screen. The sheets of squared paper included with this manual should help you design screens for input/output. Arrays and subrange types You have seen the predefined data types CARDINAL, INTEGER, CHAR, BOOLEAN, REAL etc. In this section you will see how to define data types yourself. Such data types may be given a name in a type declaration, which appears in the declaration section of the program (or procedure) TYPE name=TypeDefinition; The following is the form of a type definition for a subrange type: typename[const1..const2] The values of this type will be values of the type named (the base type), but only those values in between the two constants will be included in the subrange type. The base type must be an ordinal type, thus the subrange type is also an ordinal type. The name of the base type may be omitted, when the base type will be deduced from the type of the constants. A subrange type is compatible with its base type. The use of subrange types makes your program more readable, as it is clear what range each of the variables may take. It also helps you to find errors, as if an expression is outside the expected range, an appropriate error message will be given. A program may need to deal with several items of data of the same type, and furthermore it may be required to access each of these items by an index number, rather than giving them each a separate name. Array variables serve this purpose, and the individual items of data are called elements of the array. The 17 definition of an array type is as follows: ARRAY Indextype OF type The types here can be given as the names of types or can be type definitions themselves. The index type must be an ordinal type, and will typically be a subrange type. Note that it does not have to be numeric. To access an element of an array, its index is put in square brackets after the name of the array variable. This element may be used anywhere in your program where a simple variable might be used. For instance, an array variable may be declared as follows: VAR nums:ARRAY [1..10] OF CARDINAL when the third element of the array would be accessed as nums[3]. Note that the index does not have to be a constant; it can be any expression compatible with the given index type. A particularly common case is in a FOR loop, when the index is the FOR control variable, and so the elements of the array are processed one at a time. One important use of arrays is something you have probably felt has been missing so far; that is the storage of character strings. This is done using variables of the type ARRAY [0..C] OF CHAR where C is a constant; e.g. VAR s:ARRAY [0..7] OF CHAR; String constants can be assigned to such variables; e.g. s:='Monday'. If the string constant does not take up the whole of the array then it is terminated with a null character (CHR(0)) to mark the end. Note that the dimension of an array is fixed when the program is compiled. However it is possible to write procedures which can accept arrays of different dimensions as parameters by putting ARRAY OF typename as the type of the formal parameter instead of just the name of a type. This is called an open array parameter. Thus a procedure to deal with character strings would have parameters of type ARRAY OF CHAR. Such procedures are RDSTR and WRSTR which deal with the input and output of strings. Further procedures to deal with character strings are available in the library module STRINGS. If A is an open array parameter then the first element of the array passed to it will be mapped to A[0]. The index of the last element is given by the function HIGH(A). Note that open array parameters may be either a value or a variable parameter. The implementation of value open array parameters is not strictly correct in this compiler, as they behave in the same way as variable open array parameters, rather than 18 duplicating the array which is passed. If the type of a formal parameter of a procedure is ARRAY OF WORD, then the actual parameter may be of any type, and is interpreted as an array of bytes in the procedure. Records and the WITH statement Sometimes an item of data may have several different parts. In order for you to be able to encapsulate these into one item, it is possible to define record types, as follows: RECORD fieldname,...:type; . . END; The individual parts are called fields of the record. Note that they are declared within the record type in the same way as normal variables are declared in a program. A given field of a record variable is accessed by putting the name of the record variable followed by a dot (i.e. a full stop) followed by the field name. A frequently used data structure is an array of records. For example, the following declarations could be used to specify the corners of a triangle. TYPE coordinate=RECORD x,y:INTEGER END; triangle=ARRAY [1..3] OF coordinate. VAR r:triangle. The coordinates of the first point of the triangle t would then be r[1].x and r[1].y. Often when dealing with a record variable there will be several statements each dealing with one field of the record. To avoid having to give the record variable each time, the WITH statement is available. WITH RecordVariable DO Statements END Any reference to a field name within the statements in the WITH statement is taken as referring to the field of the record variable. By now you should be seeing how certain control structures go with certain data types. This means that if you plan the data structures of your program first, then writing procedures to manipulate this data is usually straightforward. For example, the following is a procedure to input an item of the above data type triangle. PROCEDURE InputTriangle(VAR t:triangle); BEGIN 19 WRITELN('Please enter the coordinates'); WRITELN('Press ENTER after each number'); FOR i:=1 TO 3 DO WITH t[i] DO READ LN(x);READLN(y) END END END InputTriangle; You should now write a program with similar procedures, one to print the coordinates of the triangle on the screen, and another to plot out the triangle on the screen. Enumerated types and variant records So far all the ways of defining types have been in terms of other types. The enumerated type is different, it is composed of a list of the identifiers, representing the values of the type. (name0,name1,...namen) This is an ordinal type, the order being that of the list. You may wonder what the point of enumerated types is, as you could use the numbers 0 to n instead. One point is that they can make your programs more readable, as the values can be given meaningful names. Also errors are more likely to be detected, since enumerated types are not compatible with other types. The function ORD can be used to convert a value of an enumerated type to the corresponding CARDINAL value. The reverse function is VAL, which takes the form VAL(OrdinalType,CardinalExpression) , giving a value of the ordinal type indicated. It is a common mistake to assume that the names of the value of an enumerated type can be used as strings, for instance by using WRITELN with such a value and expecting its name to be printed. This is not the case; to achieve such an effect a routine to deal specifically with the enumerated type must be written. Another mistake is to try to put values of some other type, such as characters, into an enumerated type; e.g. ('A','E','I','O','U'). Sometimes when storing information in a record variable, some fields may only be used when a particular field has a certain value. For instance, if you were storing information about vehicles in an array of records, those records relating to cars would need to store different types of information to those relating to bicycles. To give for this sort of flexibility, record types are allowed to have variants; that is, several field lists may be given and the one which applies in a particular case determined by the value of a field of the record, called the tag field. This structure goes in the record type definition, 20 and has the following form: CASE tagname:type OF const1:fieldlist| const2:fieldlist| . . ELSE fieldlist END To access components of a record variable from a field list within a variant, the tag field must have the value of the constant at the start of the variant. Again a range or list of constants may be in this position. It is permissible to omit the tag name, meaning that any of the components may be accessed. However, the variants all occupy the same storage space, so this must be used with care. Set Types Supposing you wanted to test whether a variable ch of type CHAR held one of a given set of characters, say the vowels. It would be possible to put these characters into an array and test ch against each of them in turn. However there is a more direct way, since Modula-2 allows SET types, defined as follows: SET OF OrdinalType The ordinal type is known as the base type of the set. Once a set type has been defined, a set of that type can be constructed by putting the name of the type followed by the items of the set in braces; i.e. { and }. The type of these items must be the base type of the set. A range of items may also be specified. typename{expression,expression..expression} To test whether a given item is in a set, the operator IN is used, giving a BOOLEAN result: expression IN SetExpression The expression must be compatible with the base type of the set. Thus, for the example given, letterset=SET OF ['A'..'Z'], would define the type and CAP(ch) IN letterset{'A','E','I','O','U'} would be the required test. (If ch is a lower case letter, then the value of CAP(ch) is the capital letter, otherwise it is just ch.) If v is a variable of a set type, and x is a value of the base type, then the procedure INCL(v,x) will include the value x into the set v, while EXCL(v,x) will remove x from v. The operators +, *, - and / apply to sets, denoting set union, set intersection, set difference and symmetric set difference 21 respectively. The comparison operators =, <>, <=, >= also apply to sets, testing for set equality and whether one set is a subset of another. There is no restriction, with this compiler, on the number of values of the base type of a set. However, there are restrictions on how set types whose base types have more than 16 values can be used. Firstly, a set constructor of such a type may only have constant expressions. Secondly, the operators +, -, *, / do not apply to such types; instead there are corresponding procedures in the module SETOPS. There is a predefined set type BITSET, defined as SET OF [0..7]. Set constructors type BITSET do not require the type name before the braces. Since sets are stored in memory as bitmaps, with each bit indicating whether a given value is present in the set, the type BITSET can be used in low-level programming to access the individual bits of a byte in memory. Pointer types and recursion So far, the storage requirements for all of the data dealt with by a program have had to be specified when the program is written. Sometimes it is useful to be able to allocate space for data dynamically, that is at run time rather than compile time. Such data is accessed in the program by what is called a pointer variable. Pointer types are defined as follows: POINTER TO typename Note that the type of the data item pointed to has to be specified. For example you could declare a variable VAR p:POINTER TO REAL. To create the space to store a real value and make p point you should use ALLOCATE(p,TSIZE(REAL)). This creates what is known as a dynamic variable, which is pointed to by p, and is refered to as p^. (The symbol ^ is obtained using SYMBOL SHIFT & H on the Spectrum). The procedure ALLOCATE is from the module STORAGE. (This compiler does not allow the use of NEW, as some compilers do.) Space is obtained from an area of memory called the heap. The heap is positioned between RAMTOP and the start of the Modula-2 program, which is given by the number following CLEAR after compilation. Thus you should decide how much memory is required for the heap, and CLEAR that amount below the number given. Methods of returning memory to the heap are given in the description of the module STORAGE in section 5. Using ARRAY and RECORD types you can build up many different data structures. However these all essentially have form of a 22 table of data of fixed size. Using pointer types allows you more flexibility. For instance the following would represent a tree structure of character strings. TYPE nodeptr=POINTER TO node; node=RECORD data:ARRAY [0..9] OF CHAR; leftbranch, rightbranch:nodeptr END; Note that normally an identifier must be declared before it is used in another declaration, but pointer type definitions are an exception to this rule. This allows definitions such as the one above, known as recursive definitions. Modula-2 also allows for a procedure to be recursive; that is to contain a call to itself. This can be useful when processing data structures such as the one above and for other purposes; e.g. the QUICKSORT routine in the module DATA. To fully understand the use of pointers in building data structures requires plenty of examples. If you are intersested, you will find that any book with "data structures" in its title is mainly concerned with this aspect of programming. There is a predefined pointer type called ADDRESS from the SYSTEM module, defined as POINTER TO WORD, which can be thought of as holding a memory address. This is compatible with any pointer type, and with CARDINAL, and may be mixed with values of type CARDINAL in expressions. Also, if a VAR parameter is of type ADDRESS, then a variable of any pointer type may be passed to it. The type ADDRESS allows direct access to memory addresses and so should be used with care. The function ADR applied to a variable gives a result of type ADDRESS which is the address of the variable in memory. Modules and procedure types If you look at the library modules supplied you will see that they have the form: DEFINITION MODULE name; definitions END name. IMPLEMENTATION MODULE name; body of implementation module END name. The definition part may contain declarations of constants, variables etc., just as in a program module. Only the headings of 23 procedures are put here, with the full procedure in the implementation part. The definition part indicates what can be used by programs importing the module. The form of the part following the word IMPLEMENTATION is the same as a normal program module. As well as containing the full form of the procedures declared in the definition part it may contain an initialization section. This occupies the position of the main program in a program module. The initialization sections of all of the modules imported by a program are executed before the execution of the program itself starts. The library modules supplied are essentially just collections of related procedures, but there is another reason for using modules. This is to group together the definition of a data type with the procedures acting on items of that type. For instance the module RADECSTG deals with the RA, declination, spectral type and Greek letter of a star, defining types to represent these and procedures which input and output variables of these types. In this compiler the definition and implementation parts of a module are in the same file. With other compilers they will usually be separate files, indeed the implementation part may be in compiled form, and so not readable by the user. This hiding of information about the implementation of imported procedures is an important part of good programming practice, as it enforces the independence of the separate parts of the program. As well as hiding the implementation of procedures, it is also possible to hide the internal structure of a data type, by using what is called an opaque type. This has simply the name in the definition part of the module; i.e. TYPE typename1; and the full declaration in the implementation part TYPE typename1=POINTER TO typename2;. Note that opaque types must be pointer types. As well as the above sort of module which is in a separate file to your program, it is possible to define local modules. These do not have definition and implementation parts, instead the items from the module which are for use outside the module are put in an export list. EXPORT item,...; . The form of a local module is as follows: MODULE name; imports; export list declarations BEGIN statements END name; 24 The module has an import list, as items from outside the module are not recognised inside the module unless they are imported. The statements following BEGIN form an initialization part for the module. You will have noticed that the procedures in the library modules are mostly implemented in machine code. This is done by putting HEX instead of BEGIN in a procedure. Following HEX a sequence of values in hexadecimal form are required, which are interpreted as bytes of machine code (note that these are not followed by 'H'). CARDINAL constants are allowed if placed in brackets, when they are interpreted as two bytes of code. The operator @ may be used here. This gives the address of global variables or of string constants, and the start address of procedures, each time as a constant of type CARDINAL. The procedure ends with the word END and the procedure name as usual. It is not recommended that you try to write this sort of procedure, as they require a knowledge of how the compiler passes parameters. It is hoped that a MIRA SOFTWARE Assembler package which includes a facility to simplify the writing of such procedures will be available soon. You have probably also noticed that some of the procedures available from the library modules are not declared as procedures in the normal way. Instead they are declared as variables of a procedure type. The definition of a procedure type is similar to a procedure heading, but it does not have the procedure name, or the names of parameters, just the parameter types. A variable of a procedure type may have a procedure with the appropriate heading assigned to it. The procedure variable may then be called, in the same way as a normal procedure. An example is the procedure WRSTR in the procedure STDIO. This is declared as a variable of type PROCEDURE(ARRAY OF CHAR), and has the actual procedure SWRSTR assigned to it in the initialisation section of STDIO. However, the alternative output of the module FASTO has a different procedure FWRSTR for the same purpose, and assigns this procedure to WRSTR instead. The point of this is that only one version is required of a procedure such as WRCARD from the module NUMIO, as this will call WRSTR and so use whatever method of output has been selected. Another use for procedure types would be in a graph drawing program, which obtained the function whose graph is to be drawn as a parameter of a procedure type. The type of a procedure without parameters would thus just be defined as PROCEDURE, but is given the predefined identifier PROC. 25 Concurrent Programming So far if a program has had to do more than one task, they have been executed sequentially; i.e. completing one before starting another. However there are situations where tasks need to be executed concurrently; i.e. starting a task while others are still in progress. These tasks can be thought of as being independent subprograms, and are called processes. Thus so far programs have consisted of a single process, whilst in concurrent programming the program starts as a single process which will initiate other processes. Note that since the Spectrum has a single processor, only one process can be active at a time. Transfer of activity between processes may be caused by commands in the processes themselves, or by some external cause, which for this compiler means the Spectrum's 50Hz clock. Processes are implemented as procedures without parameters. Note that whereas normally a procedure would perform a certain task and then return to the calling program, a process will typically be an endless loop, with an explicit transfer statement within it. As several processes can be active at the same time, they cannot use the same stack for storage of variables etc., so each needs a stack within its own workspace. This workspace must be large enough to store the process's variables plus those of any procedures it calls, as well as any temporary storage required. When a process is inactivated, the stack pointer is assigned to a variable of type ADDRESS. This variable can be thought of as holding the suspended state of the process. To create a process the procedure NEWPROCESS is called: NEWPROCESS(P,wsp,len,sto) P is the procedure which forms the required process. wsp and len are the start and length of the workspace for the process. The process is created in an inactive state, which is stored in the variable sto. To transfer between processes the procedure TRANSFER is called: TRANSFER(src,dst) This suspends the current process, storing its state in src and resumes the process whose inactive state has been stored in dst. In the case where transfers are to be effected by the Spectrum's 50 Hz the procedure IOTRANSFER is used: IOTRANSFER(src,dst) When called this has the same effect as TRANSFER. However, it also sets an interrupt routine so that when an interrupt occurs the 26 reverse transfer occurs, so continuing with the statement following IOTRANSFER. Typically IOTRANSFER occurs in a loop, which is executed once each time an interrupt occurs. The procedures NEWPROCESS, TRANSFER and IOTRANSFER are all from the SYSTEM module. When using interrupts it is sometimes necessary to ensure that processes are not interrupted at critical points. In the definition of Modula-2 this is done by specifying a priority in the heading of a module, meaning that it can only be interrupted by interrupts of higher priority. However, this is not implemented by this compiler. Instead, some of the procedures from the module GENERAL need to be used. The procedure DI will disable interrupts, and they can be enabled again using El. However, using these means that interrupts may be ignored. Alternatively the procedures LOCK and UNLOCK can be used. LOCK changes the interrupt routine for a routine which simply counts the interrupts. UNLOCK changes the interrupt routine back to the original one, and runs it the number counted since LOCK, thus making up for any lost interrupts. The procedure TESTRES may also be useful when using interrupts. This gets the value of a Boolean variable and sets the variable to FALSE in a single processor operation. The procedure IM1 resets the interrupt mode to 1, effectively "turning off" the interrupts set up using IOTRANSFER. 27 3: A summary of the implementation This section is concerned with some details of how the MODULA-2 language is implemented by this compiler. The ordinal types The value of MAX(INTEGER) is 32767, and MIN(INTEGER) is -32768. MAX(CARDINAL) is 65535. INTEGER and CARDINAL require two bytes of storage. The type CHAR consists of all of the 256 characters in the Spectrum character set, and all are allowed as string characters. CHAR requires a single byte. The storage required by enumerated and subrange types depends on the range of the ordinal numbers of the values of the type. If this is within 0..255 then it will need one byte, otherwise two bytes. The real types REAL (or REAL5) variables are stored in five bytes with a four byte mantissa and one byte exponent; i.e. the same as in Basic. Thus they are stored internally to an accuracy of about 10 places of decimals, of which up to 8 may be printed out. REAL2 and REAL3 variables have a one byte exponent and a mantissa of one and two bytes respectively. Sets A set is stored by the compiler by having one bit for each value of the base type of the set. The bit is set if the value is an element of the set. Thus the number of bytes used is approximately (number of values of the base type)/8. Comparisons with Pascal Modula-2 is a descendant of Pascal, and if you know how to program in Pascal, then you will easily pick up Modula-2. The main differences are listed below. 1) Modula-2 programs begin with the word MODULE rather than the word PROGRAM. 2) Most procedures used need to be imported from other modules, rather than being built in to the compiler. 3) The name of each procedure and module has to be repeated at the end of its declaration after the word END. 4) Most of the control statements finish with the word END. This means that BEGIN is only used to mark the start of the statements, not to form compound statements. 5) In Modula-2 capital and lower case letters are considered distinct. 28 Non-standard features of the implementation The following are the areas in which this implementation differs from that described in the 3rd edition of Wirth's book "Programming in Modula-2": 1) The direct specification of the position of a variable, by putting the address in square brackets, comes directly after the keyword VAR and applies to all variables declared in that section, the variables being positioned one after another. 2) Value open array parameters are not copied, but instead behave in the same way as variable open array parameters. 3) Module headings may not contain priorities. 4) If a procedure heading or type occurs in a definition module, and the full declaration is in a local module in the corresponding implementation module, then the name must be in the import list of the local module, rather than the export list. MODULA-2 Reserved Words IMPORT EXPORT FROM QUALIFIED IMPLEMENTATION DEFINITION CONST TYPE VAR MODULE PROCEDURE ARRAY RECORD SET POINTER BEGIN END RETURN IF THEN ELSE ELSIF CASE WHILE REPEAT UNTIL LOOP EXIT FOR TO BY WITH DO OF AND OR NOT MOD DIV IN Predefined Identifiers REAL REAL5 REAL3 REAL2 CARDINAL SHORTCARD CHAR BITSET PROC INTEGER BOOLEAN TRUE FALSE NIL HIGH MAX MIN SIZE CARD INT RL5 RL3 RL2 VAL ORD CHR FLOAT TRUNC WRITE WRITELN READLN HALT INC DEC INCL EXCL ABS ODD CAP The procedures WRITE, WRITELN and READLN are included in this implementation for simple input and output. READLN will input variables of the numeric types or of type CHAR. WRITE and WRITELN will output expressions of the above types, as well as 29 constant character strings. These procedures always use the standard Spectrum I/O channels, even when some other method of I/O has been selected. Compiler directives and how the compiler works Normally comments are ignored by the compiler. However, if the character immediately following the (* token is a dollar symbol $ then the comment is taken to be a directive; that is it controls some aspect of compilation, depending on the character following the $. The first of these is the letter R; i.e. (*$R *). Using this directive specifies that operators on REAL2 expressions are not required, thus freeing some memory. As was mentioned in the section on the options byte in part 2, adding 64 to (i.e. setting bit 6 of ) the options byte leads to the compiled program and its symbol table to be saved after compilation, with filenames #pname and $pname respectively, where pname represents the name of the program. If this has been done then the following directive can be used in other programs: (*$B pname*) The module specified in the directive (i.e. pname in this example) is called a base module. This directive results in the compiled code of the base module being automatically loaded into memory on compilation of the program, and it is run at the start of the program. Any modules imported by the base module will thus not need recompiling. In fact, when you are developing a program, it is best to write a base module which imports all of the completed modules which you intend to use. This way only the modules which you are actually developing need to be recompiled each time. The amount of memory available for programs on the Spectrum is fairly limited. One way of fitting in a program that is too large to fit into memory is to use overlays. To do this you should use the directive (*$O mname*). The compiled module given by mname is used as a base module. All of the modules imported into the program which do not come from the base module are compiled so as to use the same area of memory, rather than being one after the other as they would be without the directive. They are saved to the drive as they are compiled with filenames of the form %name. The program has to be written to load these modules in when they are required. Typically the module DRIVES would be part of the base module and the calls to the loading routines would be in the main program module. Using overlays in a program needs considerable 30 care; for example one overlay module cannot call a routine from another of the overlay modules. Overlays are best suited to programs which have a number of options in the main module, when each option can dealt with by a different module, or else to programs which perform a number of different tasks in sequence. The directive (*$E- *) can be used to turn off some of the run-time error checks. This will mean that the program will run faster. If you intend to use this, you are advised to put it in after the program has been debugged, rather than put it in as soon as you start writing a program. The suppression of errors may also be useful for other purposes, such as allowing addition of CARDINAL values to be done modulo 65536. Error checking may be turned on again by the directive (*$E+ *). The directive (*$E/ *) restores the error checking status to that before the last (*$E+ *) or (*$E- *). An R, B or O directive may only be placed at the start of the program module and only one may be used. The E directives may be placed anywhere in the source code. The compilation of a program, and of each of the imported modules, takes place in several passes. The first pass scans the list of modules imported by the module. Each of these must be compiled before the module itself is compiled. The second pass scans the definition part of the module, to create the corresponding part of the symbol table. The third pass scans the declarations in the module. The fourth pass is a preliminary scan of the statements, and the fifth pass determines the position of the code and variables. On the sixth pass the code is produced, completing the compilation of the module. The code is postioned after the Spectrum's system variables, and before the source code while compilation is in progress, then it is moved to the correct position when all the modules have been compiled. The positioning of each module goes backwards from the start of the run-time part of the compiler; thus the main program code, which is the last to be compiled, ends up before the code for the modules which it imports. The compiled part of each module consists of three sections; for global variables, constants and the actual machine code. After compilation, the number after CLEAR is the start of the main program's global variables, and that after SAVE is the start of the main program's constants. Note that if a module imports other modules then it may have to be loaded twice, first to scan the modules which it imports, then to compile it after these modules have been compiled. This can be avoided by working out the hierarchy of modules, then importing all 31 modules into the main program, with those at the bottom of the heirarchy last in the import list. The compiler works backwards through the import list, so those at the end will be compiled first. The symbol table While compilation is in progress the symbol table contains the declarations from the definition part of each of the modules which have been compiled, but it only contains the declarations from the implementation part of one module at a time. One problem which you are likely to run into as your programs get bigger and import more modules is running out of symbol table space. This can be seen as the symbol table overflows the screen attributes area and overwrites other memory, causing a crash. If this happens then you should check through the main module to ensure that there are no items declared which are not used. Another way of reducing symbol table size is to alter any items in the import list which have the form FROM module IMPORT item,...; to IMPORT module; and using the form module.item for the imported items. If these methods do not succeed, then a further possibility is to examine the definition parts of the modules which you are importing, and comment out those parts which you do not import into your program. Sometimes a procedure will be called without you realising it, leading to the message "UNDECLARED IDENTIFIER", and in particular it is recommended that none of the module STDIO is commented out. A similar method can be used to reduce the run-time size of the program by commenting out unused code in the implementation parts, but this tends to be more risky and is usually not worth it. 4: ERROR MESSAGES. Compile-time errors The following is a list of possible compile time error messages with their numbers, and some explanation where appropriate. 4, Out of memory : The MODULA-2 program is too long. 11, Integer out of range 44, END OF SOURCE FILE : The program is not complete. 44, RESERVED WORD 45, IDENTIFIER DECLARED TWICE 46, TYPE EXPECTED 47, TYPE MISMATCH 48, CONSTANT EXPECTED 49, VARIABLE EXPECTED 32 50, ORDINAL EXPECTED 51, FUNCTION ERROR 52, EXPRESSION OUT OF RANGE 53, RECORD EXPECTED 54, INVALID FOR VARIABLE 55, ARRAY EXPECTED 56, POINTER EXPECTED 57, NAME EXPECTED 58, SYNTAX ERROR 59, CIRCULAR MODULE REFERENCE 60, INVALID OPERATOR 61, UNDECLARED IDENTIFIER 62, SET EXPECTED 63, PROCEDURE MISMATCH 64, TERMINATOR EXPECTED 65, WRONG NUMBER OF PARAMETERS 66, LENGTH ERROR 67, UNDEFINED VARIABLE 68, INVALID NUMBER 69, MODULE NAME EXPECTED 70, NUMBER EXPECTED 71, INVALID RETURN IN MODULE 72, DRIVE ERROR 73, LOADING ERROR: Probably a bad sector. 74, SAVING ERROR : Probably the drive is full, or else the directory is full (this often causes confusing drive errors). 75, FILE NOT FOUND 76, WRONG FILE TYPE 77, ERROR FOUND 78, ERROR NOT FOUND 17, This will be followed by a message stating that a particular character, token or word is expected. Another possible error is the symbol table getting too big, which would cause the compiler to crash. As this table is stored in screen memory you can see how much space it takes. See the previous section on the symbol table to deal with this. Run-time errors The following are the possible run time errors: 1, WRONG TAG VALUE: Occurs with record variants. 2, NIL POINTER: A pointer is NIL or undefined. 3, RANGE ERROR: A variable of a subrange type has been assigned a value outside its range. 4, DRIVE ERROR: There has been an error in writing to or reading 33 from the drive. The errors HEAP OVERFLOW and DEALLOCATE ERROR may also occur if the module Storage is used. Following one of the above run-time errors, the message CONTINUE (Y/N) will appear. Pressing 'Y' means the error will be ignored, and the program will continue running, otherwise it will stop with the Basic error message "Break into program". Following this it is possible to find the position of the error in the source code by reentering the compiler, reloading the program and setting bit 5 of (ie. adding 32 to) the options byte. On pressing the 'X' key, the compiler will not produce a compiled program, but will search for the position of the error instead. If it is found then an "ERROR FOUND" message will be given, otherwise the message "ERROR NOT FOUND". In the latter case the error may have been in a base module, and so you should try again with the source code for this. Note that error messages are sometimes confusing. In the statement: WRITELN('Value is' x) a comma is missed out but the compiler will report ')' EXPECTED. In the statements: x:=y+z writeln(x) the missing semicolon will be reported at the beginning of the second line, not at the end of the first. 5: The library of modules The following pages describe what is available from the library of modules included with the compiler. For each module the definition part is given here, followed by a description of the purpose of each item. The module SYSTEM is built in to the compiler, rather than having to be loaded separately. The items from it do not follow the normal rules, but it can be thought of as having the following form: DEFINITION MODULE SYSTEM TYPE WORD; ADDRESS = POINTER TO WORD; PROCEDURE ADR(variable); PROCEDURE TSIZE(typename); PROCEDURE NEWPROCESS(P:PROC; A:ADDRESS; n:CARDINAL; VAR q:ADDRESS); PROCEDURE TRANSFER(VAR src,dst:ADDRESS); PROCEDURE IOTRANSFER(VAR src,dst:ADDRESS); END SYSTEM. 34 The modules Terminal, InOut, RealInOut, Mathlib0, LineDrawing and Processes are included with this compiler but are not described here as it is assumed that if you use them then you will be working with a book containing their description. DEFINITION MODULE STDIO; VAR RDSTR:PROCEDURE(VAR ARRAY OF CHAR); RDCHAR:PROCEDURE():CHAR; WRSTR:PROCEDURE(ARRAY OF CHAR); WRLN,CLS:PROC; GOTOXY:PROCEDURE(CARDINAL,CARDINAL); MOVEX:PROCEDURE(INTEGER); WRCHAR,CURSOR,UNCURS, MODECHNG: PROCEDURE(CHAR); GETXY:PROCEDURE(VAR CARDINAL,VAR CARDINAL); PROCEDURE KREADY():BOOLEAN; PROCEDURE GETKEY():CHAR; PROCEDURE WAITKEY():CHAR; PROCEDURE PRINTABLE(C:CHAR):BOOLEAN; PROCEDURE RDCHAR1():CHAR; PROCEDURE WRSTRLEN(S:ARRAY OF CHAR; L:CARDINAL); PROCEDURE WRSPS(NUM:CARDINAL); PROCEDURE WRSUBSTR(S:ARRAY OF CHAR; Start, End: CARDINAL); PROCEDURE RDITEM(VAR S:ARRAY OF CHAR); PROCEDURE GENRDSTR(VAR S:ARRAY OF CHAR); PROCEDURE BUFFRDCHAR():CHAR; PROCEDURE STDOUT(STRM:SHORTCARD); PROCEDURE STDIN(STRM:SHORTCARD); PROCEDURE KBDIN; (* PROCEDURE SWRSTR(S:ARRAY OF CHAR); PROCEDURE SUNCURS(C:CHAR); PROCEDURE SWRCHAR(C:CHAR); PROCEDURE SWRLN; PROCEDURE SCLS; PROCEDURE SGOTOXY(X,Y:CARDINAL); PROCEDURE SCURSOR(C:CHAR); PROCEDURE SMOVEX(N:INTEGER); PROCEDURE SGETXY(VARX,Y:CARDINAL); PROCEDURE SRDSTR(VAR S:ARRAY OF CHAR); PROCEDURE SMODECHNG(C:CHAR); *) END STDIO. The first procedures in this module are those required for input and output. These are procedure variables, and are set by this 35 module to be procedures based on the Spectrum's normal input and output channels, but can be altered to give a different method of input/output as is done by the modules FASTO and MAINREAD. To start with, the input channel is 1 (keyboard), and the output channel 2 (screen), but these can be altered using STDOUT and STDIN; for example STDOUT(3) would send subsequent output to the printer. If the input channel has been changed, then to reset it to the keyboard the procedure KBDIN should be used (rather than STDIN(1)). These procedures are also used to reset input/output procedures back to using the Spectrum's channels. The procedure RDSTR reads a line of input into a string variable, whilst RDCHAR can be used to read the input one character at a time. WRCHAR sends a character to the output, and WRSTR sends a string. WRLN moves to the next line of output. CLS is used to clear the screen. GOTOXY moves the printing position to a given position on the screen, while GETXY can be used to read the current printing position. MOVEX moves the printing position horizontally by a given number (positive or negative) of character spaces. The procedures CURSOR, UNCURS, and MODECHNG are used by the module MAINREAD. The functions KREADY, GETKEY and WAITKEY enable the program to respond to single keypresses. KREADY indicates whether a key has been pressed. If a key has been pressed, GETKEY returns the character, otherwise it returns 0C. This cannot deal with mode changes, it simply returns the key code. WAITKEY waits for a key to be pressed, and deals with mode changes. Following a mode change WAITKEY calls MODECHNG. This is set up to print the mode character ('L', 'C', 'E', or 'G') in the top right of the screen, but a different MODECHNG procedure could be written. The procedure WRSTRLEN writes a string using len character positions, putting spaces before it if necessary. If the string is longer than len then the whole string will still be written. WRSPS writes a given number of spaces. WRSUBSTR writes the substring of S starting at position start to the position end. If the end of the string comes before the position end, the appropriate number of spaces will be printed after it. Thus WRSTRLEN can be used to right justify a string and WRSUBSTR to left justify a string. While the procedure RDSTR reads everything in a line of input up to the end of line character, the procedure RDITEM ignores any initial spaces, then reads up to the next space, so it can be used to read several items on the same line. Reading a line of input from the keyboard involves buffered 36 input; i.e. the input is stored in a buffer, which is not available to the program until ENTER is pressed. The advantage of this is that it allows the input to be altered while it is being typed. For other forms of input; e.g. disc files, this is not needed, so it is better to be able to read characters one at a time. This shows why the procedure KBDIN is needed; it gives buffered input while STDIN selects unbuffered input. If you want to write different input procedures, then it is necessary to decide which of these apply. If unbuffered input is required then a procedure should be assigned to RDCHAR and then RDSTR:=GENRDSTR; will supply the required RDSTR procedure. Alternatively, for buffered input a procedure for RDSTR should be written, and then RDCHAR:=BUFFRDCHAR; will supply the required RDCHAR procedure. DEFINITION MODULE DRIVES; PROCEDURE OPEN(Drive: SHORTCARD; FileName:ARRAY OF CHAR;Channel:SHORTCARD); PROCEDURE CLOSE(Channel:SHORTCARD); PROCEDURE EOF(Channel:SHORTCARD):BOOLEAN; PROCEDURE ERASE(Drive:SHORTCARD; FileName:ARRAY OF CHAR); PROCEDURE RELOAD(Drive:SHORTCARD; FileName:ARRAY OF CHAR); PROCEDURE SAVE(Drive: SHORTCARD; FileName:ARRAY OF CHAR;START,LEN:CARDINAL); PROCEDURE LOAD(Drive: SHORTCARD; FileName:ARRAY OF CHAR;START:CARDINAL); PROCEDURE CAT(Drive:SHORTCARD); PROCEDURE RDEXISTS(FileName:ARRAY OF CHAR):BOOLEAN; PROCEDURE RDROOM(): CARDINAL; CONST D=0BD9BH; VAR ExistAction:(Error,Overwrite); VAR [D+31] Bch:CHAR; VAR [D+35] FileLen:CARDINAL; END DRIVES. This module is concerned with the the use of disc and other drives, the ramdisc, and tape. The operation of most of the procedures should be clear as they correspond to Basic commands. The drive number should be 0 if tape is required and 13 for ramdisc. A drive number of 15 will save to both drive and ramdisc, and will try ramdisc first and then the drive for loading. 37 The saving and loading procedures deal with blocks of bytes; i.e. CODE type files. There are two loading procedures: LOAD which needs the position of the code to be given; and RELOAD which loads it at the position it was saved from. The variable ExistAction specifies what action is to be taken if saving a file which already exists on the drive. This is set to Overwrite to start with, which means that the old file is replaced by the new one. Setting it to Error means that an error would be reported instead. CAT gives a catalogue of the ramdisc or drive. For most drives this will be on the current output channel. ERASE deletes a file from ramdisc or drive. OPEN connects a file on the given drive to one of the Spectrum channels and CLOSE disconnects it, so the channel number should be in the range 0 to 16. The channel can then be selected as the current input or output channel using STDIN and STDOUT from the module STDIO. The function EOF is used to test if the end of file has been reached on a given channel which has been connected to a file. The variable Bch is used by the Beta disc driver. Before calling OPEN this needs to be set to 'R' for input and 'W' for output. Before ERASE it should be set to '#' for serial files, and CHR(175) for code files. The variable FileLen will contain the length of a code file which has been loaded using LOAD or RELOAD, provided the drive is one of : +3, Microdrive, Disciple, Opus. RDEXISTS is used to test whether a given file exists on the ramdisc. RDROOM returns the amount of space available on the ramdisc in kilobytes. Note that if a 48k version is being used then using drive 13 will give an error or behave unpredictably, drive 15 will refer to drive 1, the procedure RDEXISTS will always return FALSE, and RDROOM will give 0. If you want to write a program which will work on any disc system then it is best to transfer data to and from disc by saving and loading blocks of bytes into program variables, rather than using input and output routines with OPEN and CLOSE on disc files. You will need to include the run-time drivers "RTAPE" to "ROPUS+2" with your program. After loading the program, the run-time driver appropriate to the system being used should be loaded. 38 DEFINITION MODULE FASTO; FROM SYSTEM IMPORT ADDRESS; VAR XUDGS:ADDRESS; PROCEDURE FASTOUT; PROCEDURE WRITEAT(X,Y:CARDINAL;S:ARRAY OF CHAR); (* PROCEDURE FUNCURS(C:CHAR); PROCEDURE FCURSOR(C:CHAR); PROCEDURE FWRCHAR(C:CHAR); PROCEDURE FWRLN; PROCEDURE FWRSTR(S:ARRAY OF CHAR); PROCEDURE FGOTGXY(X,Y:CARDINAL); PROCEDURE FGETXY(VAR X,Y:CARDINAL); PROCEDURE FMOVEX(X:INTEGER); PROCEDURE FCLS; *) END FASTO. This module supplies faster routines for writing text to the screen. To do this it just needs to be imported into the program. FASTOUT will reselect these routines after a different output method has been used. The speed is achieved by leaving out some facilities of normal Spectrum screen output, such as control codes for colours, etc. Also, printing will not scroll at the bottom of the screen, so it is suited to printing separate screens of text, rather than continuous printing. The routines will print characters between CHR(32) and CHR(164) in the same way as normal. CHR(165) to CHR(170) give various diagonal lines and blocks. Characters after CHR(171) can be used to print further user-defined characters stored at the address in XUDGS. The procedure WRITEAT writes a string directly to a given position of the screen. It is independent of the other routines in that it does not change the printing position, and can be used when another method of output has been selected. This means it may be useful within an interrupt routine. DEFINITION MODULE MAINREAD; VAR entrych,exitch:CHAR; textmode:(overwrite,insert,overflow); PROCEDURE EDITPREP(len:CARDINAL); PROCEDURE EDIT(VAR S:ARRAY OF CHAR); PROCEDURE EDITL(VAR S:ARRAY OF CHAR); PROCEDURE BLANK(X,Y,L:CARDINAL); PROCEDURE SETMAINRD; PROCEDURE MRDSTR(VAR S:ARRAY OF CHAR); VAR cur:CARDINAL; END MAINREAD. 39 This module allows keyboard input to take place using the upper screen. Simply importing it into a program will cause input to use the required procedures. The procedure SETMAINRD will reselect this method of input after another method has been used. It is also possible to use the procedures supplied to gain more control over the entering of text on the screen. EDITPREP(len) initialises the editing of a string at the current print position on the screen, with len specifying the maximum length of the string to be edited. Then EDIT(S) allows the string S to be edited. If you want to edit a non-empty string, then it should be printed at the appropriate position before EDIT is called. A return is made from EDIT when a non-printing character is typed, or when the string overflows the allowed length. The character causing the return is placed in the variable exitch. EDIT(S) can be called again to resume the editing of the string if required. The variable cur is the position of the cursor in the string. This is set to zero by EDITPREP, but may be altered to give an initial position which is not at the start of the string. If the variable textmode is insert, then during editing, each character typed will be inserted into the string. If it is overwrite then the character will overwrite the character at the cursor position. The mode overflow is similar to insert, but if the string overflows the allowed length then the characters at the end are lost. If the variable entrych has a non-null character on entry to EDIT then this is used as if it were the first character typed. The procedure EDITL does the same as EDIT, but a return only occurs when one of the enter, up, or down keys is pressed. If the edit key is pressed then the string is cleared. The procedure BLANK(X,Y,L) prints L spaces at the position (X,Y) then returns the print position to (X,Y). The following constants are declared within the implementation part of this module. You may find it useful to copy these to you own programs. CONST edit =07|; left =08|; right =09|; down =0A|; up =OB|; delete=0C|; enter =0D|; 40 DEFINITION MODULE STRINGS; FROM SYSTEM IMPORT ADDRESS; PROCEDURE LENGTH(S:ARRAY OF CHAR):CARDINAL; PROCEDURE CONCAT(S1,S2:ARRAY OF CHAR; VAR D:ARRAY OF CHAR); PROCEDURE SLICE(S:ARRAY OF CHAR;P,L:CARDINAL; VAR R:ARRAY OF CHAR); PROCEDURE COPY(S:ARRAY OF CHAR; VAR D:ARRAY OF CHAR); PROCEDURE APPEND(VAR D:ARRAY OF CHAR; S:ARRAY OF CHAR); PROCEDURE COMPARE(S1,S2:ARRAY OF CHAR):INTEGER; PROCEDURE INDEX1(S:ARRAY OF CHAR;C:CHAR):CARDINAL; PROCEDURE INDEX(S,SS:ARRAY OF CHAR):CARDINAL; PROCEDURE SEARCH(VAR P:ADDRESS;VAR n:CARDINAL; len: CARDINAL; S:ARRAY OF CHAR):BOOLEAN; PROCEDURE DELETE(VAR S:ARRAY OF CHAR;p,n:CARDINAL); PROCEDURE INSERT(VAR S:ARRAY OF CHAR; p:CARDINAL; S1: ARRAY OF CHAR); PROCEDURE CAPS(S:ARRAY OF CHAR); END STRINGS. This module deals with the manipulation of strings; i.e. Arrays of characters. You should read the section of the tutorial on Array types for further information. The function LENGTH(S) returns the length of the string S. CONCAT(S1,S2,D) joins together the strings S1 and S2, putting the resultant string into D. SLICE(S,P,L,R) takes the substring of S of length L starting at position P, and puts it into R. COPY(S,D) copies the string S to the string D. APPEND(D,S) joins the string S to the end of the string D. The function COMPARE(S1,S2) compares S1 and S2 using the ordering based on the ASCII values of the characters. It returns -1 if S1 precedes S2, zero if they are equal, and 1 if S1 is after S2. The function INDEX1(S,C) returns the position of the first occurrence of the character C in the string S, and the function INDEX(S,SS) returns the position of the first occurrence of the string SS as a substring of the string S. If the character or substring is not contained within the string then 65535 is returned. The function SEARCH (P,n,len,S) searches for an occurrence of the string S starting at address P, increasing P by len and decreasing n by 1 after each comparison. Searching continues until a match is found or n reaches zero. If a match is found then the function returns TRUE. 41 DEFINITION MODULE PLUS3IO; PROCEDURE PLUS3IN(FILENO:SHORTCARD); PROCEDURE PLUS3OUT(FILENO:SHORTCARD); END PLUS3IO. Since the +3 does not allow input and output from files using the normal Spectrum streams, the procedures STDIN and STDOUT cannot be used to select such input. Instead the routines PLUS3IN and PLUS30UT should be used. The file number should first have been connected to a file using OPEN from the module DRIVES, as with the other drive systems. 42 DEFINITION MODULE DATA; FROM SYSTEM IMPORT ADDRESS,WORD; PROCEDURE SWAPBYTE(VAR A,B:WORD); PROCEDURE CLEAR(V:ARRAY OF WORD); PROCEDURE FILL(V:ARRAY OF WORD;X:WORD); PROCEDURE FILL2(V:ARRAY OF WORD;N:CARDINAL); PROCEDURE MOVE(SRC,DST:ADDRESS;NUM:CARDINAL); PROCEDURE SEARCHA(VAR A:ADDRESS;VAR n:CARDINAL; I:CARDINAL;B:ADDRESS;Bsiz:CARDINAL):BOOLEAN; PROCEDURE MATCH(A,B:ADDRESS;Bsiz:CARDINAL):BOOLEAN; PROCEDURE SEARCH1(VAR A:ADDRESS;VAR n:CARDINAL; B:ADDRESS;Bsiz:CARDINAL):BOOLEAN; PROCEDURE MEMPAGE(N:SHORTCARD); TYPE SPROC=PROCEDURE(CARDINAL,CARDINAL); GPROC=PROCEDURE(CARDINAL,CARDINAL):BOOLEAN; PROCEDURE QSORT(FIRST,LAST:CARDINAL;GRT:GPROC; SWAP:SPROC); END DATA. The procedure SWAPBYTE can be used to exchange two single byte variables. CLEAR will set all the bytes of a variable to 0, while FILL will set them to the given value. FILL2 will fill the bytes in twos, using the value of the given CARDINAL. MOVE(SRC,DST,NUM) copies NUM bytes from the memory at address SRC to that at address DST. The function SEARCHA searches for the block of data at address B of length Bsiz. It starts at the address given in A, then increases A by I bytes and decreases n by 1 after each comparison. It continues until successful, or until n reaches zero. If successful the value TRUE is returned. Typically this routine would be used to search through an array of records, for the record where a certain field has a given value. The function SEARCH1 does the same as SEARCHA, for the particular case when I is 1. This can be used to search through a block of memory for a given pattern of bytes. The procedure QSORT(FIRST,LAST,GRT,SWAP) sorts elements FIRST to LAST of an array of data. GRT(i,j) must return TRUE if element i should be positioned after element j. SWAP(i,j) must swap elements i and j of the array. These procedures must be provided by the calling program. 43 DEFINITION MODULE HARDWARE; VAR fire:BOOLEAN; jsbyte:BITSET; direction:SHORTCARD; PROCEDURE JOYSTICK(n:SHORTCARD):BOOLEAN; PROCEDURE In(P:CARDINAL):SHORTCARD; PROCEDURE Out(P:CARDINAL;X:SHORTCARD); PROCEDURE KEYSCAN; PROCEDURE LOCK; PROCEDURE UNLOCK; PROCEDURE SETINTR(IP:PROC); PROCEDURE IM1; PROCEDURE WAITINT; PROCEDURE EI; PROCEDURE DI; PROCEDURE TESTRES(B:BOOLEAN):BOOLEAN; END HARDWARE. The function JOYSTICK is used to read a joystick or corresponding directions from the keyboard. The number n specifies the joystick type/keys used. For n=0 these are the cursor keys and space bar for fire. Sinclair 3 2 1 joysticks are specified by n=1 or n=2, and \ | / Kempston compatible joysticks by n=3. If the \ | / joystick is moved then JOYSTICK(n) will return 4 <---- + ---->0 TRUE and the variable direction will be set / | \ according to the diagram. The variable fire / | \ indicates whether the fire button was being 5 6 7 pressed when JOYSTICK was last called. The bits of the variable jsbyte are set as follows: 0 - fire, 1 - up, 2 - down, 3 -right, 4 - left. The procedures In and Out are the same as the corresponding Basic commands. The procedures EI, DI, LOCK, UNLOCK, TESTRES and IM1 are described in the section on concurrent programmming. SETINTR installs the given procedure as an interrupt routine. The procedure needs to be written in machine code, as it must restore any registers it uses, so it is usually easier to follow the method using IOTRANSFER instead. WAITINT simply waits for the next interrupt to occur. The procedure KEYSCAN does a scan of the keyboard, as the Spoctrum normally does every 20 milliseconds. This may be needed if you write your own interrupt routine. 44 DEFINITION MODULE BASIC; PROCEDURE RANDOM():CARDINAL; PROCEDURE RANDOMIZE(SEED:CARDINAL); PROCEDURE RND():REAL3; PROCEDURE BEEP(PITCH,TIME:REAL); PROCEDURE PAUSE(TICKS:CARDINAL); PROCEDURE PLOT(X,Y:REAL); PROCEDURE DRAW(X,Y:REAL); PROCEDURE CIRCLE(X,Y,R:REAL); PROCEDURE ARC(X,Y,Z:REAL); PROCEDURE USR(X:CARDINAL):CARDINAL; PROCEDURE BORDER(COLOUR:SHORTCARD); VAR [0BD8FH] RETVAL:CARDINAL; PROCEDURE FNARG(N:SHORTCARD):REAL; PROCEDURE FNARGSTR(VAR S:ARRAY OF CHAR; N:SHORTCARD); END BASIC. The function RANDOM() returns a random cardinal number, generated using the same algorithm as in Basic (but much quicker!). RANDOMIZE sets the seed of the random number generator. RND() returns a random number of type REAL3 in the range 0 to 1. The procedures BEEP, PAUSE, PLOT, DRAW, CIRCLE, USR and BORDER correspond to Basic commands; in fact, they call the routines in the Basic ROM. ARC gives the 3 parameter draw. Note that for fast graphics you should use the modules GRAPH1, GRAPH2 and GRAPH3. The routines FNARG and FNARGSTR allow the Modula-2 program to receive data from the calling Basic program. To do this the USR 48500 call should be put in a DEF FN command; e.g. 1 DEF FN F(X,Y,N$)=USR 48500. Then a statement such as LET L=FN F(2.5,20,"TEST") would call the compiled program. Within the Modula-2 program the function FNARG(1) will return 2.5 and FNARG(2) will return 20.0. The procedure FNARGSTR(S,3) will put the string "TEST" into the variable S. The value in the variable RETVAL is that returned by the USR 48500 call on return to Basic. Thus if the statement RETVAL:=64 was in the Modula-2 program in the above example then 64 would be assigned to the Basic variable L. 45 DEFINITION MODULE ERRORS; PROCEDURE ERROR(N:SHORTCARD); PROCEDURE ERTRAP(P:PROC):SHORTCARD; PROCEDURE EXCEPTION(N:SHORTCARD); PROCEDURE EXTRAP(P:PROC):SHORTCARD; PROCEDURE BREAK():BOOLEAN; VAR [0BD03H] ERCTRL,RERRNO,DRVERR:SHORTCARD; END ERRORS. The procedure ERROR(N) generates the Spectrum error with error number N. This will normally cause an exit from the program, and the appropriate error message to be displayed. However, the function ERTRAP can be used to trap the Spectrum errors. This executes the procedure passed to it as a parameter, and if an error occurs during this execution then a return is made from the ERTRAP function with the error number as the function result. Note that this will not usually work with drive errors which do not use the Spectrum error routines. The procedures EXCEPTION and EXTRAP act in a similar way to ERROR and ERTRAP with a return from EXTRAP being caused by a call to EXCEPTION. The function BREAK() returns TRUE if the BREAK key is being pressed when it is called. The variable ERCTRL allows control over what happens when a run-time error occurs. Normally this is zero. If it is set to 1 then errors are ignored, if 2 the Spectrum error handler will be called directly. (3 is as 2 but without the position of the error being stored). After an error RERRNO will hold the number of the error. Following a drive error the contents of the A register will be put in DRVERR, as this may indicate what sort of error has occured. DEFINITION MODULE SETOPS; PROCEDURE UNION(S1,S2,SOUT:ARRAY OF WORD); PROCEDURE INTERSECT(S1,S2,SOUT:ARRAY OF WORD); PROCEDURE DIFFER(S1,S2,SOUT:ARRAY OF WORD); PROCEDURE SDIFF(S1,S2,SOUT:ARRAY OF WORD); PROCEDURE COUNT(S:ARRAY OF WORD):CARDINAL; END SETOPS. As the compiler does not implement the set operations +, *, -, / for sets with more than 16 values in the base type, this module gives procedures which will perform these operations. The procedure COUNT returns the number of elements in a set. 46 DEFINITION MODULE STORAGE; FROM SYSTEM IMPORT ADDRESS; PROCEDURE ALLOCATE(a:ADDRESS;size:CARDINAL); PROCEDURE DEALLOCATE(a:ADDRESS;size:CARDINAL); PROCEDURE MARK; PROCEDURE RELEASE; PROCEDURE Available(size:CARDINAL):BOOLEAN; PROCEDURE SETHEAP(Start,End1:ADDRESS); END STORAGE. This module is concerned with allocating storage of dynamic variables on the "heap". If T is a type and P a variable of type POINTER TO T then ALLOCATE(P,TSIZE(T)) creates an anonymous variable of type T and makes P point to it, so that it can be referenced as P^. When the variable referenced by P^ is no longer required, DEALLOCATE(P,TSIZE(T)) will make its storage space available on the heap again. However, this is only the case if P^ refers to the variable on the top of the heap; i.e. the most recently created dynamic variable. An alternative way of dealing with heap storage is to call MARK before creating variables on the heap. A call of RELEASE reclaims the space taken since the call of MARK. The function Available(size) returns TRUE if there is space to allocate size bytes on the heap. The procedure SETHEAP can be used to create a new heap in a different place, delimited by the addresses in Start and End1 . If a new heap is created, then any dynamic variables on the old heap may still be accessed, but any new dynamic variables will be created on the new heap. 47 DEFINITION MODULE GRAPH0; CONST width=256; height=192; PROCEDURE SCRDOT; PROCEDURE SCRNAD; PROCEDURE CLS2; PROCEDURE GCLS; PROCEDURE SETATTRS(X,Y:CARDINAL;L,C:SHORTCARD); END GRAPH0. This module provides the elementary procedures used by the other graphics modules. CLS2 clears the screen, but not the attributes area, while GCLS clears the screen and attributes. SETATTRS(X,Y,L,C) sets L character squares to have attribute C starting at character position (X,Y). SCRDOT and SCRNAD are imported by other graphic modules, and are not useable as procedures on their own. DEFINITION MODULE GRAPH1; FROM SYSTEM IMPORT WORD; VAR HLINE:PROCEDURE(INTEGER,INTEGER,INTEGER); PLOT,LINETO:PROCEDURE (INTEGER,INTEGER); AREA: PROCEDURE(INTEGER,INTEGER,INTEGER,INTEGER); PROCEDURE QLINE(N,D:INTEGER); PROCEDURE SETPOS(X,Y:INTEGER); PROCEDURE POINT(X,Y:INTEGER):BOOLEAN; PROCEDURE SETMODE(C:CARDINAL); PROCEDURE SETMASK(W:WORD); PROCEDURE SETBRKN(SOLID,GAP:CARDINAL); PROCEDURE DHLINE(X1,Y,X2:INTEGER); PROCEDURE DPLOT(X,Y:INTEGER); PROCEDURE DLINETO(X,Y:INTEGER); PROCEDURE DAREA(x,y,dx,dy:INTEGER); END GRAPH1. This module defines the routines upon which graphics are based. HLINE(X1,Y,X2) draws a line from (X1,Y) to (X2,Y). PLOT(X,Y) plots a dot at (X,Y), and sets the plotting position there. LINETO(X,Y) draws a line from the current plotting 3 2 1 position to (X,Y) and moves the plotting position. \ | / AREA(X,Y,DX,DY) fills the rectangular area of size DX \ | / by DY whose bottom left corner is at (X,Y), 4 <---- + ---->0 QLINE(N,D) is a quick way of drawing a line in / | \ one of 8 directions, where N is the length in pixels, / | \ 5 6 7 48 and D specifies the direction (see diagram.) SETPOS sets the plotting position to (X,Y) without plotting anything, and POINT(X,Y) returns TRUE if the pixel at position (X,Y) is filled. SETMODE allows the setting of the graphics mode as follows: Mode 0 - unplot (i.e. plotting in white ). Mode 1 - normal plotting. Mode 2 - Xor mode (i.e. replotting a pixel resets it to white ). Mode 5 - plotting a broken line for LINETO, using a 1 byte pixel mask for HLINE and AREA. Mode 4 - As mode 5, but plotting in white. SETMASK sets the mask for modes 4&5, and SETBRKN(SOLID, GAP) sets the broken line to consist of SOLID pixels plotted followed by GAP pixels not plotted. The procedures DHLINE, DPLOT, DLINETO and DAREA are the actual procedures assigned to the procedure variables HLINE, PLOT, LINETO and AREA. DEFINITION MODULE GRAPH2; PROCEDURE CIRCLE(X,Y,R:INTEGER); PROCEDURE ELLIPSE(X,Y,RX,RY:INTEGER); PROCEDURE FCIRCLE(X,Y,R:INTEGER); PROCEDURE FELLIPSE(X,Y,RX,RY:INTEGER); PROCEDURE FTRAPEZ(XL0,Y0,XR0,XL1,Y1,XR1:INTEGER); PROCEDURE ARC0(FX,FY,THETA,OX,OY,X,Y,R,DUM:INTEGER); PROCEDURE EARC0(FX,FY,THETA,OX,OY,X,Y,RX,RY:INTEGER); END GRAPH2. CIRCLE(X,Y,R) draws a circle with centre at (X,Y), radius R. ELLIPSE(X,Y,RX,RY) draws an ellipse with centre (X,Y), horizontal semi-axis RX and vertical semi-axis RY. FCIRCLE and FELLIPSE draw a filled circle and ellipse. FTRAPEZ draws a filled trapezium, with base (XL0,Y0) to (XR0,Y0) and top (XL1,Y1) to (XR1,Y1). ARCO and EARC0 are used by the module GRAPH3. DEFINITION MODULE GRAPH3; PROCEDURE ARC(CX,CY,R:INTEGER;TH1,TH2:REAL3); PROCEDURE EARC(CX,CY,RX,RY:INTEGER;TH1,TH2:REAL3); END GRAPH3. ARC(CX,CY,R,TH1,TH2) draws an arc of a circle centre (X,Y), radius R, starting at angle TH1 radians with the horizontal, finishing at angle TH2. EARC draws the corresponding arc of an ellipse. 49 DEFINITION MODULE CLIPPING; PROCEDURE ClipOn(X1,Y1,X2,Y2:INTEGER); PROCEDURE ClipOff; PROCEDURE INRANGE(X,Y:INTEGER):BOOLEAN; (* PROCEDURE ClipLINETO(X,Y:INTEGER); PROCEDURE ClipHLINE(XL,Y,XR:INTEGER); PROCEDURE ClipAREA(x,y,dx,dy:INTEGER); PROCEDURE ClipPLOT(X,Y:INTEGER); *) END CLIPPING. This module must be imported into any program using the GRAPH modules if any plotting refers to positions which are not actually on the screen. It may also be used to clip graphics to the rectangular portion of the screen whose lower left corner is at (X1,Y1) and upper right corner is at (X2,Y2), using ClipOn(X1,Y1, X2,Y2). The procedure ClipOff turns off clipping. The function INRANGE(X,Y) returns TRUE if the point (X,Y) is within the clipping rectangle. The procedures ClipLINETO etc, are commented out, as normally they are assigned to the procedure variables LINETO etc. of the module GRAPH1. Note that clipping only applies to routines such as those in GRAPH2 and GRAPH3 using LINETO, HLINE, AREA, and PLOT; and not to QLINE, or the modules SYMBOL (or FILL). DEFINITION MODULE FILLING; PROCEDURE FILL(X,Y:CARDINAL); END FILLING. The procedure FILL provided by this module does a contour fill from the point (X,Y) out to a boundary of any shape. Note that the point must be completely surrounded, as the fill does not stop if it reaches the edge of the screen, but continues and so corrupts other parts of memory, causing a crash. DEFINITION MODULE SYMBOL; FROM SYSTEM IMPORT WORD; PROCEDURE SYMBOL1(X,Y:INTEGER;A:ARRAY OF WORD; C:CARDINAL); PROCEDURE SYMBOL2(X,Y:INTEGER;A:ARRAY OF CARDINAL; C:CARDINAL); END SYMBOL. 50 This module allows the plotting of sprite-like symbols on the screen. SYMBOL1(X,Y,A,C) will plot a symbol 8 pixels wide centred on the pixel position (X,Y). The symbol is defined by the bytes in array A, and the number of bytes in this array determines the height of the symbol. C is the plotting mode (as in the module GRAPH1), and can take values 0 (unplot), 1 (normal plotting) or 2 (xor plotting). SYMBOL2(X,Y,A,C) plots a symbol 16 pixels wide, with the pairs of bytes required obtained from the CARDINAL values in array A. These procedures do not use the graphics routines from GRAPH1, and so are not affected by the CLIPPING module. They will function correctly with the point (X,Y) off the screen, provided it is within 64 pixels of the edge of the screen. 51 DEFINITION MODULE NUMIO; PROCEDURE STRTOCARD(S:ARRAY OF CHAR; VAR OK:BOOLEAN):CARDINAL; PROCEDURE STRTOINT(S:ARRAY OF CHAR; VAR OK:BOOLEAN):INTEGER; PROCEDURE SUBSTRTOCARD(S:ARRAY OF CHAR; VAR P:CARDINAL; VAR OK1:BOOLEAN):CARDINAL; PROCEDURE SUBSTRTOINT(S:ARRAY OF CHAR; VAR P:CARDINAL; VAR OK1:BOOLEAN):INTEGER; PROCEDURE INTTOSTR(X:INTEGER; VAR S:ARRAY OF CHAR; VAR OK:BOOLEAN); PROCEDURE CARDTOSTR(X:CARDINAL;VAR S:ARRAY OF CHAR; VAR OK:BOOLEAN); PROCEDURE WRCARD(X:CARDINAL;W:CARDINAL); PROCEDURE WRCARD0(X:CARDINAL;W:CARDINAL); PROCEDURE WRINT(X:INTEGER;W:CARDINAL); PROCEDURE RDCARD():CARDINAL; PROCEDURE RDINT():INTEGER; END NUMIO. This module deals with the input and output of INTEGER or CARDINAL numbers. This involves conversion between numbers and strings, and the routines to do this are also made available. This module imports routines from the module STDIO, so it will use whatever method of input or output has been selected. WRCARD(X,W) prints the number X. If this takes less than W characters, blanks are printed before the number to make up the required length. W is called the field width. The corresponding string conversion procedure is CARDTOSTR(X,S) which puts the decimal representation of X into the string S. WRINT and INTTOSTR do the same for INTEGER numbers. The procedure WRCARD0(X,W) prints the number X but precedes it with 0s rather than spaces to make up W characters. RDCARD() is a function which returns the value of the cardinal number it reads from the input. This uses RDITEM from the module STDIO, so input stops when a space is reached. The conversion function STRTOCARD(S,OK) returns the CARDINAL number whose decimal representation is in the string S, and sets OK to TRUE if this is a valid representation of a number, and FALSE if it is not. SUBSTRTOCARD(S,P,OK1) starts at position P of the string S, and converts the characters after this position to a cardinal. For instance if P started as 0 and S was "18:30:25" then the value returned would be 3, and P would become 2. The parameter passed to OK1 is set 52 to FALSE if no valid number can be formed, otherwise it is left as it is. This means that if several substrings are being converted it can be set to TRUE at the start, then tested at the end to check they all were valid. The procedures RDINT(), STRTOINT and SUBSTRTOINT are the corresponding procedures for numbers of type INTEGER. The following three modules deal with the input and output of numbers of the three real types. They have identical form, so only the one dealing with REAL5 will be looked at. DEFINITION MODULE R2IO; PROCEDURE R2TOSTRFIX(X:REAL2;D:CARDINAL; VAR S:ARRAY OF CHAR); PROCEDURE R2TOSTR(X:REAL2;D:CARDINAL; VAR S:ARRAY OF CHAR); PROCEDURE STRTOR2(S:ARRAY OF CHAR; VAR OK:BOOLEAN):REAL2; PROCEDURE SUBSTRTOR2(S:ARRAY OF CHAR; VAR P:CARDINAL;VAR OK:BOOLEAN):REAL2; PROCEDURE WRR2FIX(X:REAL2;W,D:CARDINAL); PROCEDURE WRR2(X:REAL2;W,D:CARDINAL); PROCEDURE RDR2():REAL2; END R2IO. DEFINITION MODULE R3IO; PROCEDURE R3TOSTRFIX(X:REAL3;D:CARDINAL; VAR S:ARRAY OF CHAR); PROCEDURE R3TOSTR(X:REAL3;D:CARDINAL; VAR S:ARRAY OF CHAR); PROCEDURE STRTOR3(S:ARRAY OF CHAR; VAR OK:BOOLEAN):REAL3; PROCEDURE SUBSTRTOR3(S:ARRAY OF CHAR; VAR P:CARDINAL;VAR OK:BOOLEAN):REAL3; PROCEDURE WRR3FIX(X:REAL3;W,D:CARDINAL); PROCEDURE WRR3(X:REAL3;W,D:CARDINAL); PROCEDURE RDR3():REAL3; END R3IO. 53 DEFINITION MODULE R5IO; PROCEDURE R5TOSTRFIX(X:REAL5;D:CARDINAL; VAR S:ARRAY OF CHAR); PROCEDURE R5TOSTR(X:REAL5;D:CARDINAL; VAR S:ARRAY OF CHAR); PROCEDURE STRTOR5(S:ARRAY OF CHAR; VAR OK:BOOLEAN):REAL5; PROCEDURE SUBSTRTOR5(S:ARRAY OF CHAR; VAR P:CARDINAL;VAR OK:BOOLEAN):REAL5; PROCEDURE WRR5FIX(X:REAL5;W,D:CARDINAL); PROCEDURE WRR5(X:REAL5;W,D:CARDINAL); PROCEDURE RDR5():REAL5; END R5IO. WRR5FIX(X,W,D) prints the number X in fixed point form to D decimal places, using field width W, whilst WRR5(X,W,D) prints in floating point form. The function RDR5() inputs a number, which it returns its result. The routines to convert between strings and real numbers are also made available. R5TOSTR and R5TOSTRFIX convert a number to a string, with a given number of decimal places. STRTOR5 converts a string to a real number, and SUBSTRTOR5 converts a substring, as explained under NUMIO. DEFINITION MODULE R2MATH; PROCEDURE SIN(x:REAL2):REAL2; PROCEDURE COS(x:REAL2):REAL2; PROCEDURE TAN(x:REAL2):REAL2; PROCEDURE ARCSIN(x:REAL2):REAL2; PROCEDURE ARCCOS(x:REAL2):REAL2; PROCEDURE ARCTAN(x:REAL2):REAL2; PROCEDURE ARCTAN2(y,x:REAL2):REAL2; PROCEDURE EXP(x:REAL2):REAL2; PROCEDURE LN(x:REAL2):REAL2; PROCEDURE SQRT(x:REAL2):REAL2; PROCEDURE SQUARE(x:REAL2):REAL2; PROCEDURE POWER(x,y:REAL2):REAL2; PROCEDURE FRAC(X:REAL2):REAL2; PROCEDURE ENTIER(x:REAL2):INTEGER; (* PROCEDURE HAV(X:REAL2):REAL2; PROCEDURE ARCHAV(X:REAL2):REAL2; *) END R2MATH. 54 DEFINITION MODULE R3MATH; PROCEDURE SIN(x:REAL3):REAL3; PROCEDURE COS(x:REAL3):REAL3; PROCEDURE TAN(x:REAL3):REAL3; PROCEDURE ARCSIN(x:REAL3):REAL3; PROCEDURE ARCCOS(x:REAL3):REAL3; PROCEDURE ARCTAN(x:REAL3):REAL3; PROCEDURE ARCTAN2(y,x:REAL3):REAL3; PROCEDURE EXP(x:REAL3):REAL3; PROCEDURE LN(x:REAL3):REAL3; PROCEDURE SQRT(x:REAL3):REAL3; PROCEDURE SQUARE(x:REAL3):REAL3; PROCEDURE POWER(x,y:REAL3):REAL3; PROCEDURE FRAC(X:REAL3):REAL3; PROCEDURE ENTIER(x:REAL3):INTEGER; (* PROCEDURE HAV(X:REAL3):REAL3; PROCEDURE ARCHAV(X:REAL3):REAL3; *) END R3MATH. DEFINITION MODULE R5MATH; PROCEDURE SIN(x:REAL5):REAL5; PROCEDURE COS(x:REAL5):REAL5; PROCEDURE TAN(x:REAL5):REAL5; PROCEDURE ARCSIN(x:REAL5):REAL5; PROCEDURE ARCCOS(x:REAL5):REAL5; PROCEDURE ARCTAN(x:REAL5):REAL5; PROCEDURE ARCTAN2(y,x:REAL5):REAL5; PROCEDURE EXP(x:REAL5):REAL5; PROCEDURE LN(x:REAL5):REAL5; PROCEDURE SQRT(x:REAL5):REAL5; PROCEDURE FRAC(x:REAL5):REAL5; PROCEDURE ENTIER(x:REAL5):INTEGER; PROCEDURE SQUARE(x:REAL5):REAL5; PROCEDURE POWER(x,y:REAL5):REAL5; END R5MATH. The above three modules contain mathematical functions for the three real types. You will probably be familiar with most of them. ARCTAN2(Y,X) returns ARCTAN(Y/X) adjusted to be in the quadrant determined by the signs of X and Y, in the range -pi to pi. SQRT(X) returns the square root of X, and SQUARE(X) returns X^2. 55 POWER(x,y) returns x^y (there is no operator for this in Modula-2). ENTIER(X) returns the integer below X. Note that is the same as the Spectrum Basic function INT, whereas the function INT in this compiler truncates towards zero. For example ENTIER(-2.3) is -3, and INT(-2.3) is -2. FRAC(X) returns the fractional part of X, so FRAC(-2.3) is -0.3. The function HAV(X) is equal to (1-COS(X))/2, known as the haversine function. Some trigonometrical formulae may be expressed in terms of haversines, and doing so may reduce the loss of accuracy, which is important for the types REAL2 and REAL3. ARCHAV is the inverse function of HAV. Note that the comment brackets need to be removed if these functions are to be used. 6:General information. Much testing has been done on this compiler to ensure that it functions correctly. However, it must be understood that any such product is likely to contain bugs, and Mira Software accepts no responsibility for any loss arising from the use of this product. If you do have a problem which appears to be due to a bug in the compiler, you are encouraged to report it to Mira Software. Try to be as specific as you can, and if possible send the source program with which the bug occurs. Mira Software will try to solve the problem, and if successful then you will be sent a debugged version. All software and documentation in this product is copyright. You may make a copy of the software for back-up purposes, but making a copy to give or sell to someone else is illegal. The run-time drivers are an exception to this, and may be included with compiled programs for distribution to other people. There are no restrictions on the distribution of the compiled code of programs you have written yourself. 56