Technical Reference Manual
The Organiser Programming Language (OPL) is a high level language which has developed from a number of other languages:
The language is designed to be:
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:
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) A$(4)="AB" C(1)=12345
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 (ASCII 9).
Strings are held as the ASCII characters, numbers are held in the ASCII form. So for example after:
OPEN "A:ABC",A,A%,B,C$ A.A=12 A.B=3.4 A.C$="XYZ"
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 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$ A.B$="Hello" A.A$="AB"+CHR$(9)+"CD" PRINT A.C$ GET
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 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 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:
RTA_FP points at:
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:
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.
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:
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:
RECURS:(I%) IF I% RECURS:(I%-1) ENDIF
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:
XADD: 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 1$: LDA B,#ER_RT_NP ; wrong number of parameters SEC ; bad return RTS INCREM: LDA A,1,X ; load parameter type BNE WRGTYP ; branch if not integer LDD 2,X ADDD #1 EXIT: DEX DEX STX RTA_SP: STD 0,X ; save return value CLC ; good return RTS XXADD: 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 WRGTYP: lda b,#ER_FN_BA ; report wrong parameters type SEC ; bad return RTS
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.
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:
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 NEXT A.MAX=B.VAL USE A APPEND
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:
fins: local i%,j% do i%=menu("BANK,EXPENSES,NPV,IRR,COMPOUND,BOND,MORTGAGE,APR,END") if i%=1 : bank: elseif i%=2 : expenses: elseif i%=3 : npv: elseif i%=4 : irr: elseif i%=5 do j%=menu("VALUE,FUTURE,PAYMENT,DURATION,INTEREST,END") if j%=1 : value: elseif j%=2 : future: elseif j%=3 : payment: elseif j%=4 : duration: elseif j%=5 : interest: endif until j%=0 or j%=6 elseif i%=6 : bond: elseif i%=7 : mortgage: elseif i%=8 : apr: endif 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
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
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 GET
will print -1, i.e. TRUE.
For short machine code routines you can use this crude, but effective, procedure:
LOADR:(ADDR%,CODE$) LOCAL A%,B1%,B2%,I% A%=ADDR% I%=1 WHILE I%9 :B1%=B1%-7 :ENDIF B2%=ASC(MID$(CODE$,I%+1,1))-%0 IF B2%>9 :B2%=B2%-7 :ENDIF POKEB A%,B1%*16+B2% A%=A%+1 I%=I%+2 ENDWH
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:
MAIN: 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 ENDIF .. RETURN
MINIT: A$="3F012403CE000039" IF LEN(A$)/2>LEN(MC$) PRINT "Not enough room for MC" GET :RAISE 0 ENDIF MC%=ADDR(MC$) LOADR:(MC%,A$)
The machine code is:
OS AL$GRAB BCC 1$ LDX #0 1$: RTS
When an error is first detected the following actions are taken:
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:
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.
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:
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
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:
MAIN: 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 ENDIF IF PEEKB($2184) HELLO4: :REM call "HELLO4" if 4-line mode ELSE HELLO2: :REM else call "HELLO2" ENDIF GET
HELLO4: AT 1,4 :PRINT "HELLO"
HELLO2: 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:
Note that XTRAN will give an error if used to translate any of the above commands.