Technical Reference
Memory Usage
Filing System
System Timing
System Board
Power Supply Board
Interface Slots
Low Level
Comms Link
Psion Link
Utility System
LZ Passwords
System Services

Technical Reference Manual













The Organiser Programming Language (OPL) is a high level language which has developed from a number of other languages:

  • C ARCHIVE (the database module in Xchange)
  • PL1

The language is designed to be:

  • Fast
  • Compact
  • Flexible
  • Accurate
  • Extensible
  • Simply overlayed

The language is stack based; all code is held on the stack as are all intermediate results. To achieve speed the source code is translated into an intermediate code (Q code) before it is run.

This chapter discusses the concepts of OPL, details and examples of Q code follow in the next chapter Q-Code.



All variables in OPL are held in one of three forms:

  • Integer
  • Floating pointing
  • String

All this can either be simple variables or field variables.

All variables are zeroed when declared by a LOCAL, GLOBAL, OPEN or CREATE statements.


OPL is a procedure base language, a number of procedures normally go to make up a program. Up to 16 parameters can be passed to a procedure which always returns a variable.

When a procedure is called a header is placed on the stack, followed by space for variables declared and the Q code itself. When a procedure returns all the stack is freed for use by other procedures. This allows overlaying of code so that programs can run which are substantially bigger than the available memory on the machine.


Parameters passed to a procedure may be integer, floating point or string. They are passed by value. On the stack they are in reverse order to the order they are input.

For example the statement "PROC:(12,17.5,"ABC")" will generate the following stack entry before the procedure PROC is called:

high memory      00 12
                 00                      ; Integer type
                 00 00 00 00 50 17 01 00
                 01                      ; Floating point type
                 03 41 42 43             ; "ABC"
                 02                      ; String type 
low memory       03                      ; Parameter count


Memory addresses in OPL are held as integers. Pack addresses are held in 3 bytes. In the CM operating system the most significant byte is ignored.


An integer is a number between 32767 and -32768. It is stored in memory as a single word. In the source code of the language an integer may be input in hexadecimal by preceding the number by a '$', so $FFFF is a valid number and equal to -1.

A number in an OPL program will be taken as integer if it is in the integer range with the one exception, -32768 is taken as a floating point number. The reason for this is that the translator translates a negative number as the absolute value, followed by a unary minus operator. 32768 is outside the range for integers and so is translated as a floating point number. A small increase in speed and compactness can be obtained by writing negative integers in hexadecimal.

It is very important to anticipate what is taken as integer. For example:

30001/2 is the integer 15000 but 40001/2 is floating point number 20000.5.

To ensure that a number is taken as a floating point number just add a trailing period. '2' is an integer, '2.' is a floating point number.

The calculator translates numbers as floating point. If you wish to put an integer into the calculator you must use the function INT. So, for example, from the calculator:


passes the integer 10 to the procedure PRICE.


Floating point numbers are in the range +/-9.99999999999E99 to +/-1E-99. They are held in Binary Coded Decimal (BCD) in 8 bytes; 6 bytes for the mantissa, 1 byte for the exponent, and 1 for the sign.

The decimal number -153 is held as:

                00 00 00 00 30 15 02 80

where the last byte is the sign byte (either 00 or 80) and the preceding byte the exponent.

The decimal number .0234567 is held as:

                00 00 00 67 45 23 FE 00.

It is possible for the exponent to go out of range, e.g. 1E99*10 or 1E-99/10. This is reported as an EXPONENT RANGE error.

When floating point numbers are translated they are held in a more compact form. The first byte contains both the sign, in the most significant bit, and the number of bytes following. The next bytes are the significant bytes of the mantissa, the final byte is the exponent.

In Q code the decimal number -153 is represented as:

                83 30 15 02.

The decimal number .0234567 is represented as:

                04 47 45 23 FE

This compact form is always preceded by a QI_STK_LIT_NUM operator.


Strings are up to 255 characters long, with a preceding length byte. The string "QWERTY" is held as:

                06 51 57 45 52 54 59

All string variables, except field strings, are preceded by that variable's maximum length, as declared in the LOCAL or GLOBAL statement.

All strings in OPL have this format. For example when using USR$ the machine code should return with the X register pointing at the length byte of the string to be returned.


One dimensional arrays are supported for integers, floating point numbers and strings. Multi-dimensional arrays can be easily simulated by the use of integer arithmetic.

Like all other variables, arrays are held on the stack. In the case of string arrays the maximum string length is the first byte, the next word contains the array size, this is followed by data. So, for example,

                LOCAL A$(5,3),B%(2),C(3)

initially sets up memory as follows (from low memory to high memory):

High memory      00 00 00 00             ; 5th element of A$()
                 00 00 00 00             ; 4th element of A$()
                 00 00 00 00             ; 3rd element of A$()
                 00 00 00 00             ; 2nd element of A$()
                 00 00 00 00             ; 1st element of A$()
                 00 05                   ; array size of A$()
                 03                      ; max string length of A$()
                 00 00                   ; 2nd element of B%()
                 00 00                   ; 1st element of B%()
                 00 02                   ; array size of B%()
                 00 00 00 00 00 00 00 00 ; 3rd element of C()
                 00 00 00 00 00 00 00 00 ; 2nd element of C()
                 00 00 00 00 00 00 00 00 ; 1st element of C()
 Low memory      00 03                   ; array size of C()

After running the procedure it looks like:

High memory      00 00 00 00             ; 5th element of A$()
                 02 41 42 00             ; 4th element of A$()
                 00 00 00 00             ; 3rd element of A$()
                 00 00 00 00             ; 2nd element of A$()
                 00 00 00 00             ; 1st element of A$()
                 00 05                   ; array size of A$()
                 03                      ; max string length of A$()
                 00 00                   ; 2nd element of B%()
                 00 00                   ; 1st element of B%()
                 00 02                   ; array size of B%()
                 00 00 00 00 00 00 00 00 ; 3rd element of C()
                 00 00 00 00 00 00 00 00 ; 2nd element of C()
                 00 00 00 50 34 12 04 00 ; 1st element of C()
 Low memory      00 03                   ; array size of C()

The string and array limits are inserted into the variable space after it has been zeroed. This process is referred to as "fixing up" the variables.

Only available memory limits the size of arrays.


Automatic type conversion takes place where possible. For instance:




produce exactly the same Q code. Whereas:


has different Q code. All three place the floating point number 10 into the variable A.

When expressions are evaluated the standard left to right rule is applied with type integer being maintained as long as possible. So, for example:


generates an "INTEGER OVERFLOW" error. But:


does not. This applies to any sub-expressions inside brackets, so:


generates the overflow error.

Another side effect is that that divisions of only integers will have an integer result:

                (2/3) = 0
               (2./3) = 0.66666666667

NOTE VERY WELL: In the calculator all numeric constants are automatically converted to floating point. So in the calculator NOT(3) evaluates to 0, whereas NOT(INT(3)) is -4.

Note also: Outside the calculator a simple number is taken as an integer if is is less than 32768 and more than -32768, so in a procedure 10**10 gives an INTEGER OVERFLOW error.


NOT, AND, and OR are bitwise on integers, but on floating point numbers they are logical. So the following equalities are true:

     (NOT 3.0) = 0             (NOT 3) = -4
     (3.0 AND 5.0) = -1        (3 AND 5) = 1
     (3.0 OR 5.0) = -1         (3 OR 5) = 7

The string compares are case sensitive.


A file consists of a file name record with a number of data records.

A record contains at least one character and at most 254 characters. A record may contain up to 16 fields, delimited by the TAB character ( ')"; onMouseout="hideddrivetip()"> ASCII 9).

Strings are held as the ')"; onMouseout="hideddrivetip()"> ASCII characters, numbers are held in the ')"; onMouseout="hideddrivetip()"> ASCII form. So for example after:

                OPEN "A:ABC",A,A%,B,C$

the file buffer contains:

                 len         tab           tab
                 0A   31 32  09  33 2E 34  09  58 59 5A.

When a file is opened the field names are given. The field names and types are not fixed and may be varied from OPEN to OPEN. When a numeric field is accessed the contents are converted from ')"; onMouseout="hideddrivetip()"> ASCII to integer or floating point. Should this conversion fail the error "STR TO NUM FAIL" is reported.

When searching for a particular field the field name is matched with the field name buffer and the corresponding field split out of the file buffer using UT$SPLT.

Note that any string can be assigned to a string field but that if it includes a TAB character it will generate an extra field. For example:

    OPEN "A:ABC",A,A$,B$,C$

will print "Hello" to the screen. The file buffer contains:

                0B 41 42 09 43 44 09 48 65 6C 6C 6F

Saving data in ')"; onMouseout="hideddrivetip()"> ASCII is simple but it is easy to see how data can be compressed by using BCD, hexadecimal or other techniques.


When a procedure is loaded all the LOCALs and GLOBALs declared in it are allocated space on the stack. This area is zeroed and the strings and arrays are fixed up. In other words, the maximum length of each string and the array sizes are filled in.

These variables remain in memory at fixed locations, until execution of the declaring procedure terminates. LOCAL variables are valid only in that procedure, whereas GLOBAL variables are valid in all procedures called by the declaring procedure.


If a variable used in a procedure is not declared LOCAL or GLOBAL in that procedure it is taken as external. The Q code contains a list of externals and these are resolved at run time.

Using the frame pointer, the previous procedures are checked for all entries in the GLOBAL tables. If a match is found the variable address is inserted in an indirection table. If an external is not found it is reported as an error.

Note that neither the LOCAL names nor the parameter names are present in the Q code, but that GLOBAL names are.


There are three key pointers used by the language:

  • RTA_SP Language stack pointer
  • RTA_PC Program counter
  • RTA_FP Frame (procedure) pointer

RTA_SP points at the lowest byte of the stack. So if an integer is stacked, RTA_SP is decremented by 2 and the word is saved at the address pointed to by RTA_SP.

RTA_PC points at the current operand/operator executed and is incremented after execution - except at the start of a procedure or a GOTO when RTA_PC is set up appropriately.

RTA_FP points into the header of the current procedure.

Each procedure header has the form:

  • Device (zero if top procedure)
  • Return RTA_PC
  • ONERR address

RTA_FP points at:

  • Previous RTA_FP
  • Start address of the global name table
  • Global name table
  • Indirection table for externals/parameters

This is followed by the variables, and finally by the Q code.

RTA_FP points at the previous RTA_FP, so it is easy to jump up through all the procedures above. The language uses this when resolving external references and when handling errors.


Local variables and global variables declared in the current procedure are accessed directly. A reference to such variables is by an offset from the current RTA_FP.

Parameters and externally declared global variables are accessed indirectly. The addresses of these variables are held in the indirection table, the required address in this table is found by adding the offset in the Q code to the current RTA_FP.


Each procedure consists of two parts, a header and Q code. The Q code contains all the operands and operators in a table that is run by the TOP LOOP.

The TOP LOOP controls the language, it performs the following functions:

  1. Increment RTA_PC by the B register
  2. Test for the ON/CLEAR key
  3. Test for low battery
  4. Load and execute the next operand/operator
  5. Test carry - if set then initiate error handling



Before a file is created a check is made that no file exists with the specified name on that device. The first unused record number over $90 is assigned to the file and the file name record is written to the device. The process then continues in the same way as opening a file.
The file name records are type $81. The file name record for a file called
"AMANDA", with record file type $95 looks like:

                09 81 41 4D 41 4E 44 41 20 20 20 95


First the file name record is located to ensure that the file exists. The file record type and the device on which the file was found are saved in the file block (RTT_FILE). The field names are saved in the allocator field name cell corresponding to the logical name and the file buffer cell is expanded to 256 bytes. The record position is initialised to 1 and the first record, if it exists, is read.

If the file has just been created or the record is empty the current record will be null and the EOF flag is set.


Up to 4 files may be open at one time; to distinguish between then logical file names are used. The 4 logical file names: A,B,C, and D, are used to determine which file is to be operated on by the file commands.

This means that you can open files in any order but have a constant way of referring to them. The USE operator selects which file is affected by the following commands:

and the following functions:



There is no functional difference between the logical file names.

When opening a file the file name record and the first record are located; two cells, one a buffer and one for the field names are grown. Closing a file entails the two cells being shrunk.

All references to fields must include the logical file name. This serves two purposes; it allows statements such as "A.MAX=B.VALUE" and it allows the language to distinguish between ordinary variables and field names.


To write compact, fast code it is important to understand the way procedures are loaded and automatically overlaid.

A procedure call consists of a procedure name followed by up to 16 parameters. The procedure name may include an optional '$' or '%' but must terminate with a ':'. If parameters are supplied they must be separated by commas and be enclosed in brackets.

There are two main types of procedure. In standard OPL procedures the Q code is loaded onto the stack and then executed. The second type are known as a device procedure or language extensions; they are identical to standard procedures in appearance, but differs in that it is recognised by the device lookup and runs as self-contained machine code.


When a QCO_PROC operator is encountered the parameters will already be on the stack, along with the parameter count and the parameter types. After the operator is the name of the procedure.

The following list of actions are then carried out:

  1. Check if it is a language extension/device call
  2. Search for the procedure starting with the default device
  3. Check that there is sufficient memory
  4. Set new RTA_SP, RTA_FP
  5. Check the parameter count
  6. Check the parameter types
  7. Set up a table of variables declared GLOBAL
  8. Set up the parameter table
  9. Resolve the externals, build an externals table
  10. Zero all variable space
  11. Fix-up strings
  12. Fix-up arrays
  13. Load the code
  14. Set new RTA_PC

The code is loaded every time a procedure is called. This means that recursive procedures are allowed but that the stack will grow by the size of the Q code + data space + overhead for each call. On an XP, following a Reset, the procedure:

                IF I%

allows values up to 315 before an 'OUT OF MEMORY' error is given.


Language extension are also referred to as device procedures. Examples are LINPUT, LSET and LTRIG in the RS232 interface.

To test if a procedure is a language extension, call DV$LKUP. This looks through the devices loaded in order of priority. If a language extension is found it returns with carry clear, the device number in the A register and the vector number in the B register, suitable for an immediate call to DV$VECT to run the code.

The machine code should check that any parameters that have been passed are correct, do whatever it has to do, add the return variable to the stack and return. It is essential to return the right variable type. If the extension name terminates with a '$' it must return a string, if with a '%' it requires an integer, otherwise an 8 byte floating point number.

Note that a variable number of parameters can be passed to a device.

As a simple example, consider a language extension to add two integers without giving an error if the sum overflows. If only one parameter is given the value is simply incremented, again without giving an error. The assembler for this extension called "ADD%" is:

        LDX     RTA_SP:
        LDA     A,0,X
        BEQ     1$                      ; wrong number of parameters
        DEC     A
        BEQ     INCREM                  ; increment 1 parameter
        DEC     A
        BEQ     XXADD                   ; add the two
        LDA     B,#ER_RT_NP             ; wrong number of parameters
        SEC                             ; bad return
        LDA     A,1,X                   ; load parameter type
        BNE     WRGTYP                  ; branch if not integer
        LDD     2,X
        ADDD    #1
        STX     RTA_SP:
        STD     0,X                     ; save return value
        CLC                             ; good return
        LDA     A,1,X
        BNE     WRGTYP                  ; branch if not integer
        LDA     A,4,X
        BNE     WRGTYP                  ; branch if not integer
        LDD     2,X                     ; and add the two integers
        ADDD    5,X     
        BRA     EXIT
        lda     b,#ER_FN_BA             ; report wrong parameters type 
        SEC                             ; bad return


Like any programming language there is an infinite number of approaches to every problem. The aim should be to produce fast, compact Q code that runs in a minimum of memory but is also easy to write and understand. These aims inevitably conflict with each other; the correct balance varies from application to application.

For example, the decision to use a separate procedure, rather than writing the code in line, is a matter of considering the difference in Q code size, the extra stack required at run time, the time overhead required to load and return from a procedure and finally style.

It is impossible to give definitive rules on writing code but it is worth taking the following points into account.


  1. Only use procedures where appropriate
  2. If it makes no difference, use Locals instead of GLOBALs
  3. Use short field names
  4. Use short global names
  5. If you repeatedly use a CHR$ with the same value, assign it to a variable
  6. Use "RETURN" instead of "RETURN 0" or "RETURN """
  7. Use hexadecimal integers instead of negative integers


  1. Write short Q code (as above)
  2. Use a small main procedure to call several small procedures.
  3. Use integers instead of floating point numbers
  4. Use short field names
  5. Use short global names
  6. Check the deepest part of the code by adding, temporarily, PRINT FREE :GET. Then consider restructuring the procedures to decrease the amount of stack used.


  1. don't use too many procedures, regard them as being similar to overlays
  2. place the procedures at the beginning of the pack, with the most frequently used at the start
  3. Use Locals or GLOBALs rather than field variables
  4. Don't use procedures inside time critical loops, write the code in-line
  5. Use integers instead of floating point numbers
  6. Write short Q code (less code to load)
  7. Use Locals instead of GLOBALs

Each operand/operator has an overhead of .05 ms. Most integer based operands/operators are very fast and run in less than .1 ms.

The following timings are rough and should only be used as a guide:

OPERANDTime (ms)
RND 10
AT 0.15
PRINT a string 0.5
TAN 350
ATAN 170
SQR 240
EXP 130
LOG/LN 200
Integer add/subtract 0.1
Integer multiply/divide 1
Floating point add/subtract 3
Floating point multiply 10
Floating point divide 20
Accessing a field 5

PRINT_CR has a default delay of 500 milliseconds. This value can be altered by poking the value in DPW_DELY.


The smallest time overhead on loading, and returning form a procedure is 8 ms. This overhead increases if the procedure follows other blocks or records on the device. It also increases if the procedure is not on the same device as the top level procedure (as it will have to search that device first).


Some of the file operators have to count up the pack each time they are used. For the sake of speed NEXT remembers its position on each of the packs. However it only remembers one position on each pack so:

                USE B
                USE A

where file A is on B: and file B on C: is significantly faster than if they are both on the same device.

BACK however always has to count up the pack to locate a record and this can take a noticeable time. Remember that erased records, as well as readable ones, will slow down the location of a record.


Before starting to write a program (which normally will consist of a number of procedures) first decide the relative importance of speed of execution, compactness of the Q code and the amount of stack used.

Then rough out the procedure structure. For example, in the case of the finance pack the main procedure is called FINS:

local i%,j%
  if     i%=1 : bank:
  elseif i%=2 : expenses:
  elseif i%=3 : npv:
  elseif i%=4 : irr:
  elseif i%=5
      if     j%=1 : value:
      elseif j%=2 : future:
      elseif j%=3 : payment:
      elseif j%=4 : duration:
      elseif j%=5 : interest:
    until j%=0 or j%=6
  elseif i%=6 : bond:
  elseif i%=7 : mortgage:
  elseif i%=8 : apr:
until i%=0 or i%=9

Your style may vary if you are writing on the emulator or the ORGANISER itself. On the ORGANISER it is worth, as a general rule, making only limited use of the ':' option to have more than one statement on a line. On the emulator you may prefer to write multiple statements on a line. The procedure above was written using a full screen editor which is reflected in the elegant use of non-functional spaces.

It is very helpful to indent the code by logical function. This is very useful in matching IF/ENDIF and loop commands.

Comment the code. The logic may seem very obvious when you write it but other people may want to read it, or you may return to the code after several months. In most cases the extra space taken by the comments is well worth it. Remember that comments make no difference to the Q code size.

Use brackets if you are unsure of the operator precedence. This adds nothing to the Q code size but makes your intentions absolutely clear.

When using the ':' separator it is not necessary to precede it by a space when the preceding characters cannot be taken as a variable name. So "A%=1:B%=2" is valid but "A%=B%:B%=C%" gives a syntax error. It can, however, save time and make the code more readable if you always proceed the ':' separator with a space.


The translator scans the source code, statement by statement, translating it into Q code. All expressions are converted to reverse polish (postfix) form so that, at run time, the operators can be executed as soon as they are encountered.

It is beyond the scope of this document to describe the detailed working of the translator. Fortunately, such a description is not necessary in order to understand either the execution of the code or the writing of efficient code.



Runs the language by loading and running the OPL procedure. The procedure can not have any parameters.


Runs the translator, either

  • translating language procedures
  • translating CALC expressions
  • locating errors in CALC
  • locating errors in language procedures
LN$XSTT (LZ only)

Acts like LN$STRT except that there is a choice whether the source is to be translated in 2-line mode or

LG$ENTR (LZ only)

Provides an entry point to the PROG application in the top-level menu. There are 2 functions available:

Function 1 - Call the PROG application as from the top-level menu.

Function 2 - Search block files of a given type that are on packs for a given string.


From the information in this chapter, the programmer knows exactly where everything is on the stack.

When variables are declared they are used in order, so:

                LOCAL A%,B% 
                PRINT ADDR(A%)=ADDR(B%)+2

will print -1, i.e. TRUE.

For short machine code routines you can use this crude, but effective, procedure:

                LOCAL A%,B1%,B2%,I%
                WHILE I%9 :B1%=B1%-7 :ENDIF
                  IF B2%>9 :B2%=B2%-7 :ENDIF
                  POKEB A%,B1%*16+B2%

When calling this procedure you must pass the machine code in digital form and the address where to put the machine code. It is essential that the programmer ensures there is enough room for the machine code at the address given.

A calling sequence might look like:

                GLOBAL MC%,MC$(10)
                MINIT: :REM Initialise the machine code
                CELL%=USR(MC%,100) :REM GRABs a cell of size 100
                IF CELL%=0 
                 PRINT "No cell free"
                 GET :RAISE 0
                IF LEN(A$)/2>LEN(MC$)
                  PRINT "Not enough room for MC"
                  GET :RAISE 0

The machine code is:

                OS      AL$GRAB
                BCC     1$
                LDX     #0
        1$:     RTS



When an error is first detected the following actions are taken:

  1. The error saved in RTB_EROR
  2. If the TRAP flag is set then the language continues
  3. The ON_ERR address for that procedure and each procedure above is tested. If one is found to be non-zero, RTA_PC is set to that value and RTA_SP set to the BASE_SP for that procedure. The language then continues on.
  4. If no error handling is detected then the error is reported along with the name of the procedure in which the error was detected and the language exits.

If the error is ER_RT_UE (undefined external) then the externals which are undefined are displayed with DP$VIEW.

If the error is ER_RT_PN (procedure not found) then the name of the procedure not found is displayed (as well as the procedure where it was called).


Every time round the top loop the difference between RTA_SP and ALA_FREE is calculated. If this difference is less than 256 bytes, "OUT OF MEMORY" is reported. Note that no operand or operator can grow the stack by more than 256 bytes.

The filing system can also generate the "PACK FULL" error if it detects that after an operation fewer than 256 bytes will be free on device A. In this case it means essentially the same thing as "OUT OF MEMORY".

The only time when OPL uses memory, other than on the stack, is when it opens files.


If the voltage goes below the threshold value (5.2 volts) while the language is running, it is detected either in the top loop or during the execution of an operator. In either case it is treated as a standard error. If no error handling is in force, the error is reported and the machine turns off.

If the error is handled by an ONERR, the low battery error number is saved in RTB_EROR. It is not reported again by the top level until the battery voltage has gone back above the minimum voltage. This allows the procedure to take some action (e.g. to turn the organiser off). If the procedure just continues on the battery will eventually die completely and there is a risk of having to cold boot the machine.

Note that the battery is more likely to drop below the threshold voltage when devices, such as the packs or the RS232 interface, are switched on because they drain substantially more current than the Organiser by itself. See section power supply for more details of the power drain of different devices. Also note that a battery naturally recovers some of its power after being turned off for a while.


In normal operation pressing the ON/CLEAR key results in the execution of the language being frozen until another key is pressed. If the key pressed is 'Q','q' or '6' it creates an error condition ER_RT_BK. If there is no user error handling, execution of the language will terminate.

If ESCAPE OFF has been executed then the ON/CLEAR key has no special effect.

In an input statement then the ON/CLEAR key acts in one of 3 different ways:

  1. If there is any input it is cleared
  2. Or if the TRAP option has been used then the input exits with the error condition ER_RT_BK
  3. Otherwise it is ignored


OPL is a powerful flexible language and as such it has the potential to crash the operating system or get into an infinite loop. This is particularly unfortunate in the case of the ORGANISER because all the data held in device A: is lost when the machine reboots. For extensive development of 'dangerous' routines a RAMPACK has a lot to recommend it.

There are trivial ways to crash such as poking system variables or using USR function with wrong addresses or bad machine code. It is impossible to describe all the other ways in which such problems can arise. The examples listed below show the most obvious ways in the simplest possible form.

  • ESCAPE OFF :DO :UNTIL 0 :REM Impossible to get out
  • WHILE GET :ENDWH :REM Hard to get out of
  • DO :KEY :UNTIL 0 :REM Hard to get out of
  • A:: ONERR A:: :RAISE 0 :REM Impossible to get out
  • A:: ONERR A:: :DO :UNTIL 0 :REM Impossible to get out

Error handling is best added at the end of a development cycle. Turning ESCAPE OFF substantially increases the chances of getting into an infinite loop from which there is no exit.



The LZ is fully back-compatible with 2-line Organisers, so any existing OPL programs will run exactly the same on the LZ as they do on the CM or XP but will use 2 lines in the center of the screen with a border around.

When OPL programs which have been translated on a 2-line Organiser (CM, XP, etc.) are run either from the top level menu or under PROG the machine is automatically put into "2-line compatibility mode".

The mode in which the OPL program runs is determined by the first OPL procedure run. Any subsequent OPL procedures which are loaded will run in the same mode.

The OPL object code generated when translated on an LZ contains a STOP code followed by a SINE code at the front of the procedure. Thus the object code of all 4-line procedures will be two bytes longer than the same procedure translated on a 2-line machine. The STOP/SINE configuration is used for 2 reasons:

  1. The combination will identify the procedure as 4-line since it can never be generated on a 2-line Organiser.
  2. When run on a 2-line machine, the procedure will cause OPL to stop running, i.e. terminate the OPL program.

Note that LZ machines can translate procedures as though they were translated on a 2-line Organiser using the "XTRAN" option in the PROG EDITOR menu.

It follows from this that 2-line code can run in 4-line mode providing the initial procedure is 4-line, but 4-line code can never run in 2-line mode.


To create an OPL application which will run on both types of machine
and make use of all 4 lines when run on an LZ, use the following method:

  1. The main procedure should be translated in 2-line mode and should contain the following code:
        LOCAL M%(2)
        M%(1)=$3F82 :REM OS DP$MSET
        M%(2)=$3900 :REM RTS
        IF PEEKB($FFE8) AND 8)=8 AND (PEEKB($FFCB) AND $80)=$80
          USR(ADDR(M%()),256) :REM switch to 4-line mode if LZ
    This code will switch to 4-line mode if it is run on an LZ but will do nothing if run on a 2-line machine.
  2. Code which can be used on both 2 and 4 line machines (generally code that does not print to the screen or use LZ only features) should be put in subroutines and translated in 2-line mode.
  3. Code to print to the 2 line screen should be put in one procedure and code to print to the 4 line screen in another - translated in 2-line mode and 4-line mode respectively.
  4. The main code must read DPB_MODE (address $2184) to decide which OPL procedures to call to print in the correct mode.

For example the following program will print "HELLO" on the 2nd line of a 2-line machine and on the 4th line of an LZ. The first two modules have to be translated in 2-line mode and the 3rd one in 4-line mode:

    LOCAL M%(2)
    M%(1)=$3F82 :REM OS DP$MSET
    M%(2)=$3900 :REM RTS
    IF PEEKB($FFE8) AND 8)=8 AND (PEEKB($FFCB) AND $80)=$80
      USR(ADDR(M%()),256) :REM switch to 4-line mode if LZ
    IF PEEKB($2184)
      HELLO4: :REM call "HELLO4" if 4-line mode
      HELLO2: :REM else call "HELLO2"
    AT 1,4 :PRINT "HELLO"
    AT 1,2 :PRINT "HELLO"


OPL has been extended on the LZ. Some existing commands and functions have been extended to use the 4 lines (e.g. AT 20,4, VIEW(4,A$) etc.) and some new commands and functions have been added.

The OPL commands and functions which are not available on the CM or XP are as follows:

  • OFF x% - Turns the Organiser off for a limited time only.
  • UDG - Defines a display character (user-defined graphic).
  • COPYW - Copies any type of file with wild card matching.
  • DELETEW - Deletes any type of file with wild card matching.
  • CLOCK - Displays the "UDG CLOCK".
  • MENUN - Displays the different types of menu.
  • DIRW$ - Returns name of any type of file with wild card matching.
  • FINDW - Like FIND but allows the use of wild cards.
  • ACOS - Returns the arc cosine of a number.
  • ASIN - Returns the arc sine of a number.
  • MAX - Returns the greatest item in a list.
  • MIN - Returns the smallest item in a list.
  • MEAN - Returns the mean of items in a list.
  • STD - Returns the standard deviation of items in a list.
  • SUM - Returns the sum of items in a list.
  • VAR - Returns the variance of items in a list.
  • DAYS - Returns the number of days since 01/01/1900
  • DAYNAME$ - Converts 1 - 7 to the day of the week.
  • DOW - Returns the day a date falls on as a number 1 - 7.
  • MONTH$ - Converts 1 - 12 to the month.
  • WEEK - Returns the week number a date falls in.

Note that XTRAN will give an error if used to translate any of the above commands.