Dec 182017
Large Interger Arithmetic Language version 9.3.
File LIAL93.ZIP from The Programmer’s Corner in
Category Miscellaneous Language Source Code
Large Interger Arithmetic Language version 9.3.
File Name File Size Zip Size Zip Type
FIB.L 2516 949 deflated
LASM.COM 2736 2210 deflated
LDEBUG.BIN 3646 2760 deflated
LIAL.BIN 3456 2859 deflated
LIAL.DOC 35284 11562 deflated
LRUN.ASM 8764 2841 deflated
LRUN.BIN 384 357 deflated
LUCLEH.L 10133 3399 deflated
PI.L 8186 2929 deflated
RABIN.L 8241 2818 deflated
RSA.L 33412 9584 deflated

Download File LIAL93.ZIP Here

Contents of the LIAL.DOC file

LIAL93.ZIP contains following files:

LRUN ASM Sample custom program
LRUN BIN Binary for previous, excluding interpreter
LDEBUG BIN LIAL debugger, excluding interpreter
LIAL BIN LIAL self-relocating interpreter
LASM COM LIAL assembler
FIB L Program to compute Fibonnaci numbers
LUCLEH L Program to find Mersenne primes
PI L Program to compute pi to high precision
RABIN L Program to find large primes probabilisticly
RSA L Routines for public-key cryptography
LIAL DOC Documentation (this file)

Create additional files LRUN.COM and LDEBUG.COM as follows:


I. Introduction

This package contains an assembler and debugger for an assembly-style language
to manipulate large integers and, by fixed point techniques, high precision
fractional values. The language has no I/O routines, since its programs are
intended to be embedded, along with the interpreter, in custom calling
programs. The debugger is such a program. It has limited I/O facilities and
may be used to load/run/save programs and edit/view their output. A good
familiarity with assembly language programming is expected here. You may
incorporate the interpreter and LIAL programs in your own work, if you like.

The goal of the language is to allow you to write programs that manipulate
large integers independently of their size. That is, a typical LIAL program
has header assignments that specify the large integer size and the total
storage space to use. The program then usually (but not necessarily always)
goes about its business without reference to the size of the data being
manipulated. The program PI.L will calculate pi to a length of 1 byte or 8192
bytes, depending on the header assignments. The running time, of course,
varies considerably, but the program is otherwise identical. This lets you
focus on the algorithm you are using while leaving many of the cluttered
details to the interpreter.

A few sample source programs have been included as models. The simplest is
FIB.L, which computes the largest Fibonnaci number that fits the declared
large integer size. This is a good place to get your feet wet.

Assemble the program with LASM FIB (creating FIB.LOB), then load the result
into the debugger with LDEBUG FIB. Execute the program with go (G), then
display (D) storage (S). This shows the largest 64-byte Fibonacci number in
hex format, beginning EC3... Restart the program with go, then display
storage again. The result is converted to packed decimal format in the first
128 bytes, including leading zeroes. The 155-digit number begins 123... Quit
(Q) the program. You may fiddle with the source code, e.g., changing the
large integer size, to experiment further.

The other sample programs are similarly ready to assemble and run, except for
RSA.L. RSA.L contains a set of cryptographic routines requiring z-register
and large integer storage input. You may modify the program to pre-supply the
necessary input for testing, if you wish. Note, however that the RSA process
is patented. See program comments in the source file.

II. Overview

LIAL (Large Integer Arithmetic Language) is designed specifically to handle
large integers. By use of fixed point techniques, real number computations
are also possible (e.g., see the sample program PI.L, which computes pi to
high precision). If you look at the sample programs, you will see that the
language has the look and feel of an assembly language.

A LIAL program consists of a 128-byte header, the program code itself, then
the large integer storage area. The total size of these three is limited to
65535 bytes. It is usually best to force an even offset for storage by
insuring that the program code is an even number of bytes and (if you write
your own calling program) insuring that the header starts at an even offset.
The header contains miscellaneous program state information, including

register values, subroutine stack, and program address labels.

The language provides for a large integer accumulator and up to 16 large
integer variables. The large integer size may be initialized by assembler
directive and may be modified later under program control. The accumulator
and 16 variables are contiguous in the large integer storage area. A change
in large integer size in effect just reduces or expands the portion of the
storage area that is referenced (always starting from the low end), without
changing or shifting storage contents.

Besides large integers, the language also has a single-byte general purpose z-
register and 16 single-byte auxiliary registers. These are useful as flags,
as loop counters, and for manipulating large integers in byte-size units. The
z-register is available for some limited arithmetic operations in conjunction
with the large integer accumulator. LIAL also maintains a carry bit and an
overflow bit, as well as a four-byte seed for generation of pseudo-random

Program flow is controlled by branches, subroutine calls, jumps, and a switch
instruction. Branches are based on the status of the carry and overflow bits,
the large integer accumulator, and the z-register. Branches are restricted to
the range [-128, 127] bytes from the instruction immediately following the
branch instruction. The LIAL assembler will let you know if an attempted
branch is out of range. Up to 32 subroutine/jump labels are also allowed,
with nesting to 16 levels deep for subroutines. A jump instruction can be
used instead of a branch instruction, if the branch would be out of range.
The switch instruction is a forward branch the number of bytes specified in
the z-register, with the maximum-number-allowed specified in the instruction
operand. The instruction allows a case-like structure within LIAL programs.

LIAL supports a basic complement of arithmetic and logical operations on large
integers. These operations principally affect the contents of the large
integer accumulator. The only arithmetic/logical instructions that directly
affect the contents of a large integer variable as well as the accumulator are
the multiplication, division, and conversion instructions (binary to packed-
decimal, and visa versa, for integers and fractions).

LIAL also supports one custom-tailored instruction--the UDF instruction, which
executes a user-defined function. The debugger defaults this to a NOP, but if
you include the interpreter in your own program, the UDF near offset is passed
as an argument to the interpreter (along with the offset of the header/
program/storage area).

Most assembled LIAL instructions are a single byte long and the longest
instructions are two bytes long.

The language has no input/output instructions (aside from possibly the UDF
instruction). LIAL is strictly a computational language. Input is provided
by initializing registers and/or large integer storage prior to executing
programs. This may be done with assembler directives or else may be done by
the program which calls the LIAL interpreter. Output is handled by having the
calling program monitor the final state of the registers/large integer storage

The calling program provided with this package is the debugger LDEBUG.COM. It
allows manipulation of the LIAL program and data before, during, or after
program execution. This is accomplished through pre-programmed breaks (END
instructions) or through breaks enabled in the run environment.

You may write and compile a custom calling program to handle I/O differently.
The program LRUN.ASM is included to demonstrate this.

III. Instruction Set

The instructions below are listed in order of increasing opcode. The first 16
instructions (MUL through BMI) are two-byte instructions. The rest are one-
byte instructions.

For the MUL and DIV operations, VAR1 and VAR2 (both 0-15) refer to long
integer variables, the contents of which are treated as non-negative integers
for the operation. VAR1 and VAR2 may not refer to the same variable (the
assembler insures this). E.g., to square an integer, the value must be copied
to separate long integers (and the accumulator zeroed) before multiplying.

For the DIV operation, the overflow bit is set and no other action occurs if
the contents of VAR2 are zero or if the quotient would not fit into VAR1.
Otherwise the bit is cleared and division proceeds normally.

If you edit a MUL or DIV instruction (e.g., via the E option in LDEBUG) so
that VAR1 and VAR2 are the same, the result in this version of the interpreter
is treated as a two-byte NOP instruction.

MUL VAR1 VAR2 Accumulator (high), VAR1 (low) <-- VAR1 * VAR2 + accumulator
DIV VAR1 VAR2 Accumulator <-- remainder, VAR1 <-- quotient of
accumulator (high), VAR1 (low) / VAR2

For LAI and LZI, BYTE must be 0 - 255. The acronyms here stand for load
accumulator immediate and load z-register immediate.

LAI BYTE Accumulator <-- BYTE (leading bytes zeroed)
LZI BYTE Z-register <-- BYTE

For the SWZ instruction, LABEL must be 0 - 255 bytes from the instruction that
immediately follows the switch. The assembler checks for this. The switch is
a forward branch of z bytes, with LABEL as the default maximum destination.
No run time check prevents switching, branching, or jumping to the middle of a
two-byte instruction. This is legal, but not recommended practice.

SWZ LABEL PC <-- minimum of LABEL and PC + z-register

For the branch instructions, LABEL must be within a displacement of -128 to
+127 bytes from the instruction immediately following the branch. The
assembler checks for this.

BVC LABEL PC <-- LABEL if overflow clear
BVS LABEL PC <-- LABEL if overflow set
BCC LABEL PC <-- LABEL if carry clear
BCS LABEL PC <-- LABEL if carry set
BZE LABEL PC <-- LABEL if z-register is zero
BZN LABEL PC <-- LABEL if z-register is non-zero
BEQ LABEL PC <-- LABEL if accumulator is zero
BNE LABEL PC <-- LABEL if accumulator is non-zero
BPL LABEL PC <-- LABEL if accumulator greater equal zero
BMI LABEL PC <-- LABEL if accumulator less than zero

As with MUL and DIV, the operands for MLZ and DVZ are treated as non-negative
integers. For the DVZ operation, the overflow bit is set, the z-register
returns 255, and no other action occurs if the z-register is initially zero.
Otherwise the bit is cleared and division proceeds normally.

MLZ Z-register (high byte), accumulator (low bytes)
<-- accumulator * z-register
DVZ Z-register <-- remainder, accumulator <--
quotient of accumulator / z-register
LEN Accumulator <-- bit-length of accumulator, less the number
of leading zero bits (approximate binary log)
ZER Accumulator <-- 0
INC Accumulator <-- accumulator + 1
DEC Accumulator <-- accumulator - 1
NEG Accumulator <-- - accumulator
RLC Rotate carry bit left into accumulator and back into carry
RRC Rotate carry bit right into accumulator and back into carry

The RND instruction extracts pseudo-random bits from the seed register by the
feedback shift-register method and changes the register in the process. The
seed register should be set to a non-zero value before RND instructions are
executed, or else the RND instructions will return zero. The assembler
defaults the seed to one.

RND Accumulator <-- pseudo-random number less equal
original accumulator (considered non-negative)
RRZ Rotate z-register right into accumulator back into z-register
RLZ Rotate z-register left into accumulator back into z-register

For conversion operations, the accumulator is the source and the first long
integer (0) is the destination. Both the source and destination will change
for the BDI and BDF operations. Just the destination changes for the DBI and
DBF operations, but its initial value is also input to the operation.

Note that a second call to the BDI or BDF instructions may be required to
extract remaining significant digits, unless the source is known initially to
contain more than about 17% leading/trailing zeroes for the integer/fraction
conversion. Similarly, for the DBI/DBF operations, the source may be
contained in more than one long integer, even though the result will still fit
the destination slot. So, more than one conversion call may be required.
Begin all DBI conversions with the most significant source digits, and begin
all DBF conversions with the least significant source digits.

N below refers to the long integer size in bytes. For fraction conversions
(DBF and BDF), the fixed point is assumed left of the source/destination.

See PI.L and FIB.L for sample use of BDF and BDI, respectively.

BDI Convert binary integer to packed decimal
Source <-- integer part of source / 100**N
Destination <-- fractional part of same in
packed decimal format
DBI Convert packed decimal integer to binary
Destination <-- source (converted to binary) +
destination * 100**N
BDF Convert binary fraction to packed decimal
Source <-- fractional part of source * 100**N
Destination <-- integer part of same in packed decimal
DBF Convert packed decimal fraction to binary
Destination <-- source (converted to binary) +
destination / 100**N

END End LIAL program (break)
RTS Pull PC from stack (return from subroutine)
LSZ Left shift z-register into 4-byte seed register
CLZ Z-register <-- 0 (clear z-register)
INZ Z-register <-- z-register + 1
DEZ Z-register <-- z-register - 1
SLZ Shift carry bit left into z-register into carry
SRZ Shift carry bit right into z-register into carry

The LNH and LNL instructions allow the large integer size to be changed within
a LIAL program. The contents of large integer storage are unaffected by these
instructions. Always execute LNH before LNL instead of vice versa. The LIAL
interpreter always insures that the large integer size N is at least 1. This
may have an unintended effect when trying to change N to a value that is a
multiple of 256, if the LNL instruction is executed first.

LNH N (high byte) <-- z-register
LNL N (low byte) <-- z-register
CLC Clear carry bit
SEC Set carry bit

For the ADZ and SBZ instructions, the overflow bit is set if overflow occurs;
otherwise the bit is cleared. In either case, the operation is performed.

ADZ Accumulator <-- accumulator + z-register

SBZ Accumulator <-- accumulator - z-register

The UDF instruction executes a user-defined function. See the Custom Use
section for more info. LDEBUG treats the instruction as a NOP.

UDF User-defined function
NOP No operation performed

For the JMP and JSR instructions, any LABEL within the program bounds is
allowed. A total of 32 JSR/JMP labels are allowed, but at most 16 of these
may be JMP destinations.

JSR LABEL Push PC onto stack, then PC <-- LABEL

For the LDZ and STZ instructions, AUXREG (0-15) refers to an auxiliary byte-

LDZ AUXREG Z-register <-- AUXREG
STZ AUXREG AUXREG <-- z-register

For the remaining instructions, VAR (0-15) refers to a long integer variable.
The overflow bit is set or cleared by the ADD and SUB instructions, depending
on the outcome of the operation.

EOR VAR Accumulator <-- accumulator ^ VAR
ORA VAR Accumulator <-- accumulator | VAR
AND VAR Accumulator <-- accumulator & VAR
SUB VAR Accumulator <-- accumulator - VAR
ADD VAR Accumulator <-- accumulator + VAR
LDA VAR Accumulator <-- VAR
STA VAR VAR <-- accumulator
EXA VAR VAR <--> accumulator (exchange)

IV. Assembler

The LIAL Assembler, LASM.COM, assembles LIAL source code into object code that
may be loaded and executed by LDEBUG.COM. The source file name is assumed to
have an extension of L and should be provided without extension as the only
argument to LASM. The resulting object file has the same file name, but with
extension LOB instead of L. The object file has a 128-byte header preceding
the assembled code and an optional trailer containing initial data for the
long integer storage area. Assembler rules and conventions follow.

Variable names, constants, and labels must begin with a letter and may contain
only letters and digits. They may be as long as desired (up to the limit
imposed by line length), but only their first eight characters distinguish
them from each other. The assembler is not case-sensitive.

Besides spaces, which delimit symbols, LASM recognizes these special

'*' begins lines with initial header data assignments,
'#' begins lines with symbol assignments,
'%' begins lines with initial trailer data,
';' begins comment lines,
'$' immediately precedes hexadecimal literals, and
'=' assigns constants, variable names, and header information.

Forward references in assignments are allowed only to statement labels.

The left side of header data assignments must be one of the following symbols:

FLAGS, STORAGE, PC, N, Z, SEED, R0, R1, ..., R15.

The FLAGS byte holds bit flags which enable/disable user breaks, program
bounds error breaks, storage bounds error breaks (bits 0-2), Mul-Div breaks,
single step breaks, trace breaks, and external breaks (bits 4-7). One bit is
unused. See the Break Flags section for description of these breaks.
Normally, you should assign $7 to FLAGS to enable the first three breaks.

STORAGE may be specified up to 65535-128 less the program size.

PC (program counter) and N (large integer size) are two-byte integers, SEED
(for pseudo-random numbers) is a four-byte integer, and the rest are one-byte
integers. Z initializes the z-register and R0 - R15 initialize the auxiliary

The SEED value may be initialized with a 4-byte literal value. Symbols are
otherwise limited to two bytes.

N, SEED, and STORAGE each default to one. The (32 maximum) JSR/JMP labels in
the header are assigned automatically by the assembler, as is the two-byte
program size specification. Other header data defaults to zero.

The header symbols are not reserved, and may be used as variable names,
constants, or labels.

Only one LIAL instruction or assembler assignment is allowed per line, and any
trailing information in a line is treated as comment (except for trailer data
lines, which are scanned to the end for successive bytes). The default for
large integer storage outside of that specified in trailer data lines is
undefined, although LDEBUG.COM insures that it is zeroed.

Blank lines are ignored and any lines longer than 127 characters are split up
into logical lines of 127 characters each plus a last (shorter) line
containing the excess.

The symbol table generated by the assembler is limited to 512 entries. This
table is displayed after a successful assembly, along with the size of the
newly-created object file.

V. Debugger

The LIAL debugger, LDEBUG.COM, loads a LIAL object file and then executes
interactive commands. These commands run the program, save the results to
disk, and perform the debugging functions described below. The object file
name is assumed to have extension LOB and should be provided without extension
as the only argument to LDEBUG.

If the object file was successfully loaded, the prompt "Enter Command (? =
Help):" appears. Enter an question mark for a display of the LDEBUG options.
These are described next.

G -- Go. This starts (or restarts, after a break) execution of the program.
Execution continues until the next break is encountered. Note that a break
due to an END instruction points the program counter to the following
instruction, so that a subsequent restart picks up from there.

U -- Unassemble program. This option displays a disassembly of the LIAL
program 32 lines at a time.

R -- Register display. This option extracts some header information and
displays it in a more readable form than the hex display of the header in the
dump option. The next instruction to be executed is also shown.

S -- Single step. This option causes the next program instruction to be
executed, followed by a halt. The single step bit (bit 5 in FLAGS) is set to
force the break. The prior setting of the bit is then restored.

T -- Trace to address. This is like the G option, except that you specify an
end address at which to halt execution. The trace check is not made for the
initial instruction, so you may trace loop repetitions without moving off the
current instruction by an interposed single step. The trace break bit in
FLAGS (bit 6) is set during the run. The prior setting is restored afterward.

E -- Edit header, program, or storage. This option allows you to hex edit
these data regions. If you edit the program, it is normally a good idea to
use option U to display a disassembly of the program afterwards. Similarly,
if you edit the header region (a brief header map is displayed), you should
use option R to display register contents afterwards and inspect your changes.
If you edit the program size in the header, a corresponding adjustment is made
to the start of the storage region (which immediately follows). The storage
data itself is not shifted.

D -- Dump header, program, or storage. This displays the header, program, or
storage contents in hexadecimal format. A breakdown of the header contents
follows (this map is displayed in abbreviated form before the dump):

Offset Contents
------ ------------------------------------------------------------------
0 Result 0-7 of last break (RESULT).
1 Break flags (FLAGS) in bits 0-2 and 4-7 -- bit 3 unused.
2-3 Size of assembled program (PGM SIZE).
4-5 Size of large integer storage area (STORAGE).
6-7 Program counter (PC).
8-9 Large integer size (N).
10 Z-register (Z).
11 Stack pointer in bits 0-3, overflow (VBIT) in bit 6,
and carry (CBIT) in bit 7 -- bits 4 and 5 are unused.
12-15 Pseudo-random seed (SEED).
16-31 Auxiliary registers (R0-R15).
32-63 Subroutine return address stack, split high/low in 16-byte halves.
64-127 JSR/JMP labels, split high/low in 32-byte halves, with JSR labels
assigned from top to bottom and JMP labels from bottom to middle.

Register values and large integer values are stored lowest memory offset to
highest memory offset with most significant byte to least significant byte.

W -- Write program state. The current state of the program (header, assembled
program, and declared large integer storage area) is saved to disk under the
same file name as the input object file. If the program has finished, this
preserves the final program state. Final register values (in the header) and
final larger integer values (in the large integer storage area) may be
extracted later, as desired. If the program has not finished, this preserves
the program state for a reload and restart later (which is done using LDEBUG
the same way the program was initially loaded and run). The file size will
usually be larger than that originally output from LASM, since all of declared
storage is included.

A -- Auto-write toggle. This option is useful when running time-consuming
programs. When the toggle is on, a running program (started by the G or T
options) is interrupted every 15 minutes, the current program state is saved
to the LOB file (as in the W option), and the program is restarted. Timer
interrupt 9h is trapped for this purpose (18.2 ticks per second assumed). At
each 15 minute mark, the external break bit (high FLAGS bit) is set to halt
the program. Afterward, the prior setting of the bit is restored. The time
is cumulative over successive program restarts.

Q -- Quit. This exits the debugger. If you wish to save the program state,
do so with the W option beforehand.

VI. Break Flags

The FLAGS assembler directive and/or an LDEBUG edit of the flags byte allows
seven break flags to be enabled/disabled. Typically, you need only be
concerned with the first three. Break checks are only made before or after
instructions. Long-running MUL or DIV instructions may not be interrupted.

The last four break checks described below do not kick in until after one
instruction has been executed. The first three checks will also precede the
initial instruction.

Bit 0. User break flag. When user breaks are enabled, the LIAL interpreter
checks for the presence of keyboard input before every instruction. If any is
found, the program is interrupted. The interpreter checks the BIOS keyboard
buffer head/tail pointers directly. This is the only external reference made
by the interpreter (which contains no BIOS or DOS interrupt calls).

Bit 1. Program exception break flag. When this break is enabled, the
interpreter checks the program counter before executing each instruction. If
an instruction is out of range, the program is interrupted. This break, for
example, will occur if you attempt to restart a program which has been halted
with an END instruction that is physically the last instruction in the
program. Normally, you should have this break flag enabled until your LIAL
program is debugged.

Bit 2. Storage exception break flag. When this break is enabled, the LIAL
interpreter checks each instruction prior to execution to see if any large
integer data outside of the large integer storage area might be modified. If
so, the program will be interrupted. Reading data outside of the large
integer storage area will not cause a break. This break will occur, for
example, if the large integer size (N) is 16, large integer storage (STORAGE)
is 256, and you attempt to execute STA LAST, where LAST = 15 (the last long
integer variable). Note that the large integer accumulator occupies the first
16 bytes of large integer storage, and large integer variables 0 - 14 occupy
the next 15 * 16 = 240 bytes. As above, you should make sure this break flag
is enabled until your LIAL program is thoroughly debugged.

Bit 3. Unused.

Bit 4. Mul-Div break flag. When Mul-Div breaks are enabled, the interpreter
halts program execution after each MUL or DIV instruction is executed. You
might wish to use this flag in a custom program to count and/or log time marks
at each MUL/DIV. These are typically the two instructions that consume most
program execution time.

Bit 5. Single step break flag. Enabling this break causes a LIAL program to
be interrupted after each instruction is executed. The debugger uses this
flag for single stepping.

Bit 6. Trace break flag. When this flag is set, the interpreter compares the
current program counter (address of next instruction to be executed) with the
trace break address that is passed as the third argument to the interpreter.
If there is a match, execution is halted. The address argument is relative to
the program offset (not to the header offset). The debugger uses this flag
for its trace option.

Bit 7. External break flag. When the LIAL interpreter detects this bit set,
execution is halted. If you set the flag, e.g., during assembly, this is
effectively the same as single stepping. However, the purpose of the flag is
to allow an external process to set the flag in the LIAL header while a
program is running. The debugger does just this when the auto-write toggle is
on. Note that ONLY the flags byte may be modified by an external process
while the interpreter is running. Header and storage data is temporarily
adjusted during program execution (e.g., byte swaps to force Intel low/high
order), so you may not make assumptions about data locations.

The LIAL interpreter returns 0 (in the AX register) when an END instruction is
encountered. The other breaks have return values of 1 - 7, respectively. The
return value is also stored in the RESULT byte (first byte) of the header.

VII. Timing Notes

Most large number manipulation is bound by multiplication/division steps. The
interpreter uses classical order n-square algorithms. These are not the
fastest possible algorithms, but within this constraint things are reasonably
optimized. That is, word operations are used in preference to byte
operations, the innermost multiplication/division loop is unrolled for eight
iterations, and there is some optimization for leading/trailing zeroes. Odd
large integer sizes are word-optimized to the extent possible (although there
is some penalty). For small sizes (less than 16 bytes), byte algorithms are
used for mul/div operations until performance gains by word algorithms offset
the extra overhead. About half the interpreter is devoted to multiplication
and division. The code is not 386-optimized.

For very large integers, a MUL or DIV instruction takes about 50 * n * n clock
cycles, where n is the word length of the operands. That is, on a 486/33MHz
machine and with 2048-byte operands (1024 words), a MUL or DIV operation takes
about 50 million cycles, or 1.5 seconds. The sample program PI.L will have
better performance than this due to optimization for leading/trailing zeroes.

Aside from the conversion instructions (BDI/BDF/DBI/DBF), the MUL and DIV
instructions are the only order-n-square operations. LIAL programs should use
shifts or MLZ/DVZ (multiply/divide by z-register--an order-n operation) when
possible to improve performance. PI.L, for example, substitutes shifts and
MLZs for MUL instructions. LUCLEH.L uses similar techniques.

Odd long integer lengths and odd initial offsets for long integers will also
decrease performance (up to about 15% in the worst cases). The interpreter
itself also functions best at an even offset.

If the speed is not sufficient for your needs, you still might find the
package useful for preliminary testing against your own procedures.

VIII. Custom Use

This section describes interpreter calling protocol from ASM programs and the
calling protocol by the interpreter to a user-defined function. It has been a
few years since I wrote anything in C, so although the protocols below are C
protocols (from an old pre-ASM version of this), I am no longer familiar with
the equivalent C compile/link procedures.

The LIAL interpreter is contained in LIAL.BIN. This is a self-relocating
module. Relocation fixups are made on initial call to the interpreter, which
is entered at the top. The easiest way to include the interpreter in a COM
program is just to append it to the COM file, with the program reserving the
necessary space and noting the start (call) offset. An even offset is best.

The interpreter uses a small memory model C protocol calling convention, with
the SS=DS requirement relaxed. The first argument is the offset in the DS
segment of the header/program/storage data. The second argument is the offset
in the CS segment of a UDF routine. This may be dummied by pointing to a near
return instruction. The third argument (optional) is the LIAL program offset
(relative to the program, not the header) at which a trace break, if enabled,
will halt execution.

By C protocol, these arguments are pushed in reverse order prior to calling
the interpreter and are cleared by the calling program afterwards. Also by C
protocol, the interpreter preserves all registers except ES/DX/BX/CX/AX/Intel
flags. On exit, return value AX is 0 - 7 (see Break Flags above) and AL is
also saved in the first header byte. You may rely on ES=DS and CLD set on
exit from the interpreter. The interpreter uses about 100 stack bytes. It is
not required that SS=DS.

The UDF routine should also observe C protocol. A copy of the first LIAL
argument is pushed prior to a near call to your UDF and is cleared by the
interpreter afterward. Your UDF should preserve the same registers noted
above. On entry to the call, you may additionally rely on ES=DS and CLD set.
If your program sets SS differently from DS, your UDF should likewise allow
for this.

The UDF may modify any header/program/storage data, provided the result is
still a legitimate program state. The warning earlier about external breaks
does not apply here, since the data is tidied up prior to a UDF call. Be
particularly careful about changing the declared program/storage size. A UDF
may be used, for example, to prompt for and save a new z-register value. Or
else you might write an optimized routine to perform a particular function
(e.g., modulo exponentiation) faster than an equivalent LIAL routine.

See the sample custom program LRUN.ASM. It demos a call to the interpreter
and contains a commented-out sample UDF. The comments at the program start
also give the necessary MASM/LINK/EXE2BIN/COPY steps to create the final COM
file. LDEBUG.COM was created in a similar fashion.

Both LRUN and LDEBUG reserve a full 64K segment for LIAL data, since they are
designed to handle any LIAL program. If your custom ASM program runs only a
specific LIAL program, you need only assure there is space for the header,
program, and its declared storage (provided it is thoroughly debugged or
provided the necessary bounds checking is turned on). This may simplify
things by allowing you to leave the COM segment registers at their defaults.

E.g., your startup code CUSTOM.ASM can note the start offsets for LIAL.BIN and
WIDGET.LOB and can insure both the stack and any buffers you need are clear of
the WIDGET data area. Then your final step in COM file creation could be:


You might try this out by writing a short ASM program that calls the
interpreter to run PI.LOB and then displays the result from the LOB storage
area (128+122 bytes ahead of the header offset). At 256-byte precision, you
get about 614-digit accuracy. The digits are contained in the first 307
storage bytes in packed decimal format.

This package is an update of a mix of C/assembly programs from 1988, with
everything now converted to assembly language.

Craig Hessel/ER Support/Department of Energy/26 Sep 93

 December 18, 2017  Add comments

Leave a Reply