An arbitrary-precision calculator

From PROSE Programming Language - Wiki
Revision as of 16:30, 3 October 2017 by Cambridge (Talk | contribs)

Jump to: navigation, search

PAL Tutorial - An arbitrary-precision calculator

In release 0.11.x, only the PROSE Assembly Language (PAL) is available, and then only a subset of those instructions. So be aware, it's very low-level programming at this time. To learn more about the PROSE Programming Language, visit http://prose.sourceforge.net.

It is suggested you begin with the following articles before attempting this tutorial.

A full list of available tutorials for the PROSE Assembly Language can be found on the tutorials index page.

This tutorial works through an example PAL program that implements a simple arbitrary-precision calculator.

Objectives

The objective is to write a PAL program that will allow for simple two-operand calculations on integers, floating-point numbers and rational numbers. This will demonstrate the use of variables and functions as well as basic integration with the GNU MP (Multi-Precision) library.

The program will understand the following commands supplied on the standard input:

    M+N             adds two numbers M and N and displays the result
    M*N             multiplies two numbers M and N and displays the result
    M-N             subtracts two numbers M and N and displays the result
    M/N             divides two numbers M and N and displays the result
    M~N             divides two numbers M and N and displays the result
                        (when the number type is rational)
    int             set number type to integer
    flt             set number type to floating-point
    rat             set number type to rational
    help            displays this help page
    debug           toggles debug mode ON and OFF
    quit            exits this program

Integers, floating-point numbers and rational numbers will all take the form accepted by the GNU MP library. Floating-point numbers at this time have a default precision set to 128 bits. Rational numbers are represented by X/Y where / separates the numerator from the denominator. Because of this, the ~ symbol will be used by the program for requesting a division operation with rational numbers.

Step 1 - Displaying a title

The first step is to display a title when the program is launched. This should be straightforward if you have followed previous tutorials:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Simple arbitrary-precision calculator
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
._init
func/def	[main],     &[.main]
func/def	[title],    &[.func_title]
local/rtn

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% main()
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.main
func/call	NULL, [title]
func/rtn

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% title()
%
% Display title of program to stderr
%
% Arguments:
%	None
%
% Returns:
%	None
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.func_title
attr/mod	![.prose.sys.io], [psStreamError], [
pscalc: a simple arbitrary-precision calculator
Type 'help' for a list of commands or 'quit' to exit
]
func/rtn

Step 2 - Accepting basic commands

The help and quit commands should be easy to implement. We need a function which we'll call getcmd() that reads the next command from standard input and strips the trailing newline character. This demonstrates returning a value from a function, and introduces us to the concept of the encoded attribute value (or XVALUE). Here is the function code:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% getcmd()
%
% Gets next command from stdin (stripping newline character)
%
% Arguments:
%	None
%
% Returns:
%	String, or empty string if input is exhausted
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.func_getcmd
reg/load	P15, ![.prose.sys.io]

% Display command prompt to stderr
attr/mod	P15, [psStreamError], [> ]

% Collect input
attr/copy	P0, P15, [psStreamIn]
reg/jmpeq	&[.getcmd_null], P0, NULL

% Strip trailing newline character
reg/xload	P1, (P0)
op/decr		P1
error/jmp	&[.no_strip], ![.prose.error.sys.OutOfRange]
reg/load	P2, (P0, P1)
op/shr		P2, P2, #24
reg/jmpneq	&[.no_strip], P2, #10
reg/save	P0, (P1)
func/rtn	P0

.no_strip
error/clr
func/rtn	P0

.getcmd_null
func/rtn	[]

The only difference in the above code from previous tutorials is that a string is being passed to the func/rtn instruction. This necessitates an additional argument to the corresponding func/def instruction which we insert into the ._init section. We'll re-write that section now to pre-load some attribute definitions into registers P0 and P1:

._init
attr/load	P0, [psString], P1, [psInteger], P2, [psBoolean]

%
% Function definitions
%
func/def	[main],     &[.main]
func/def	[title],    &[.func_title]
func/def	[getcmd],   &[.func_getcmd],	P0
local/rtn

Here the psString attribute is being passed to the 3rd argument of func/def. This identifies the type of data that the function will return. If omitted, or NULL, then the function cannot return a value. Function return values can only be a variable type, i.e. a type that can be assigned to a program variable. We discuss variable types later on in this tutorial.

When the getcmd() function is called, it will return the next line from standard input. All functions that return values will return them as encoded attribute values. This is data that is encoded for a specific attribute type, in this case psString. It will be saved into a register as the type PSUNIT_TYPE_XVALUE. When the func/rtn instruction is used with a value, that value can be provided in the correct encoding (in which case it is simply returned as-is), or if not it will be re-encoded automatically by the PROSE engine before the return completes. This re-encoding will usually take place via a byte string representation, because all variable types can be represented in byte string format.

This concept is worth demonstrating. Before continuing, try calling the getcmd() function and dumping the result, e.g. after a call to title() but before the main() function returns:

func/call	P0, [getcmd]
reg/dump	P0
obj/dump	P0

Notice the first argument to func/call. In previous tutorials this has been NULL. By providing a register you are specifying where the return value should be stored. Executing our program at this stage and typing the quit command should produce the following output:

pscalc: a simple arbitrary-precision calculator
Type 'help' for a list of commands or 'quit' to exit
> quit
register: P0
type: PSUNIT_TYPE_XVALUE (0x15)
psString

quit

The reg/dump instruction informs us that register P0 contains an encoded attribute value. The value is encoded according to the psString syntax. Essentially this is a value detached from an object attribute, but which could be attached at a later time. The value reported by obj/dump is 'quit' as expected, because this is the command you typed.

Remove the three lines added above and let's add proper support for the help and quit commands.

We'll use a data segment to map commands to an appropriate code label that can handle the command. This data segment can appear anywhere in the code, but we can put it ahead of the .main section:

%
% Mapping of code labels to commands
%
~cmdlist
EQUP { &[.quit] }; EQUS { [] }
EQUP { &[.quit] }; EQUS { [quit] }
EQUP { &[.help] }; EQUS { [help] }

We want empty input or the quit command to be routed to the .quit label, while the help command should be routed to the .help label. This requires a loop in the main() function after the title has been displayed:

.main
func/call	NULL, [title]

.loop
%
% Fetch command from stdin
%
func/call	P0, [getcmd]

%
% Test for one of the known commands
%
reg/load	P1, ( &[~cmdlist] )

.cmdloop
reg/load	P2, (P1)
reg/jmpeq	&[.cmdloop_end], P2, NULL
reg/load	P3, (P1)
reg/jmpneq	&[.cmdloop], P0, P3
reg/clr		P1
local/jmp	P2

.cmdloop_end
% No known command, just ignore and loop for now ...
reg/clr		P0, P1
local/jmp	&[.loop]

This uses the reg/load instruction in one of its indirect addressing modes, in order to iterate the values within a data segment. This is described in the reg_load_segment(5) man page.

The .quit and .help sections can follow immediately on:

.quit
% Exit program
reg/clr		P0
func/rtn

.help
% Display help message
reg/clr		P0
attr/mod	![.prose.sys.io], [psStreamError], [
M+N		adds two numbers M and N and displays the result
M*N		multiplies two numbers M and N and displays the result
M-N		subtracts two numbers M and N and displays the result
M/N		divides two numbers M and N and displays the result
M~N		divides two numbers M and N and displays the result
			(when the number type is rational)
int		set number type to integer
flt		set number type to floating-point
rat		set number type to rational
help		displays this help page
debug		toggles debug mode ON and OFF
quit		exits this program
]
local/jmp	&[.loop]

Notice we're choosing to display informational messages to psStreamError (the standard error stream). This is to keep the standard output clear for the mathematical results so that commands can be piped into the program and results extracted easily by an external command.

Step 3 - Switching number types

This calculator will support integers, floating-points and rationals. These are specified by the attribute types psInteger, psFloat and psRational respectively. The calculator will accept the commands int, flt and rat to switch between these types. We'll use a global variable to store the current number type.

Program variables in PAL are objects in the nexus, and as any other object in the nexus they are defined using classes and attributes. Typically a variable has the psVariable class as well as one other variable-type class, and a variable-type attribute containing the value. The variable-type classes and variable-type attributes are named identically and are listed in the ps_attributes(5) and ps_classes(5) man pages. Those which we will be using are:

  • psFloat - GMP floating-point number
  • psIndex - raw index (32-bit unsigned integer)
  • psInteger - GMP integer
  • psRational - GMP rational number
  • psString - string data

Ordinary object create commands could be used for creating program variables, e.g. obj/def, class/add, attr/add and obj/commit. However, for convenience, the PAL instruction set comes with some short-cut instructions for creating variables:

  • var/local - create a local variable
  • var/static - create a static variable
  • var/global - create a global variable
  • var/def - create a variable in an arbitrary location

These will assign the correct classes and attributes to the object and commit it to the nexus. The only difference between the above instructions is the location in which the variable object is created. A local variable will be created underneath the instance container of the running function, and therefore each function instance will have its own value. A static variable will be created directly underneath the running function object, and as a result each function instance will share the same value. A global variable will be created underneath the module root, and therefore has module-level scope. This is described fully in the var/def(5) man page.

To create the numtype global variable, add the following instruction after the title has been displayed but before entering the main loop:

var/global	NULL, [psString], [numtype], [psInteger]

This creates a variable at the module level called numtype that will contain a psString value (i.e. a byte string). The initial value contains the text 'psInteger' because we want this variable to define the current number type to use in calculations. It will default to integers. The first argument is NULL meaning that we don't need a pointer to the new variable object at this time. We can use the var/addr instruction later on to obtain it.

If you are interested in seeing the structure of the new variable object, rather than discarding the pointer with a NULL keyword, store it in a register and then use obj/dump on it. The register contains a pointer to a node (PSUNIT_TYPE_NODE) and therefore obj/dump will display the classes and attributes assigned to that node. For example:

var/global	PUSH, [psString], [numtype], [psInteger]
obj/dump	PULL

would display the following output when the program is run:

pscalc: a simple arbitrary-precision calculator
Type 'help' for a list of commands or 'quit' to exit
.prose.code.default.numtype:objectClass=psVariable
.prose.code.default.numtype:objectClass=psContainer
.prose.code.default.numtype:objectClass=top
.prose.code.default.numtype:objectClass=psString
.prose.code.default.numtype:pn=[numtype]
.prose.code.default.numtype:psString=[psInteger]
>

This tells you that it's a variable (psVariable) that it's a string (psString) and the value of the string ('psInteger').

We will write a function called settype() that takes the name of the attribute type to set as a string (one of psInteger, psFloat or psRational) and which will set the global numtype variable and report the new number type to the standard error stream. It will require a definition in the ._init section like this:

func/def	[settype],  &[.func_settype],	NULL, P0, [type]

The 3rd argument is NULL as this function will not return a value. The 4th and 5th arguments define the first parameter accepted by the function, by type and name. The 4th argument is the register P0 which we set earlier to contain the psString attribute definition and specifies that the parameter is a string type, and the 5th argument is type meaning that when the function is invoked the value passed into this parameter position will be assigned a local variable with this name.

Now comes the function itself, which can be appended to the end of the program code:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% settype()
%
% Sets global number type
%
% Arguments:
%	type = name of attribute type to set: psInteger, psFloat or psRational
%
% Returns:
%	None
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.func_settype
var/addr	P0, [type]
attr/copy	P0, P0, [psString]
reg/copy	P2, P0
var/addr	P1, [numtype]
attr/mod	P1, [psString], P0
reg/copy	P2, [Number type: ], P2, [\n]
attr/mod	![.prose.sys.io], [psStreamError], P2
func/rtn

Because the function definition identified one formal parameter (a string variable called 'type') then when the function is called a single argument will be expected. That argument will be re-encoded to a string type (if required) and a local variable called type will be automatically created underneath the instance container, in exactly the same way as if the var/local instruction had been used.

The var/addr instruction works in a similar way to func/addr in that it uses a search path algorithm to identify the named variable. In the above example, var/addr P0, [type] will look for a variable object called type and store a pointer to that node in the P0 register. First a local variable will be tested for with that name, then a static variable and finally a global variable. If a variable of that name cannot be found, a runtime error will be generated.

Then we use the attr/copy instruction to get a copy of the psString attribute value assigned to the variable object. This will yield a PSUNIT_TYPE_STRING. Alternatively, we could have used attr/xcopy if we wanted a PSUNIT_TYPE_XVALUE. Following this instruction, register P0 holds the value of the first argument passed to the function.

If you were to insert a reg/dump P0 after the var/addr and again after the attr/copy you would see the difference between the two instructions. The output would look like this:

register: P0
type: PSUNIT_TYPE_NODE (0x01)
.prose.code.default.settype._i0#0.var.type

register: P0
type: PSUNIT_TYPE_STRING (0x04)
psFloat

Now we take the value from the local type variable and use it to overwrite the value in the global numtype variable. Then we report the action to psStreamError.

Finally, to complete this step, we need to plumb in the int, flt and rat commands. To do this, add the commands to the cmdlist data segment like this:

EQUP { &[.int] }; EQUS { [int] }
EQUP { &[.flt] }; EQUS { [flt] }
EQUP { &[.rat] }; EQUS { [rat] }

and add corresponding .int, .flt and .rat sections that each call settype() with the appropriate argument, this code can be inserted immediately before the .quit label:

.int
% Set number type to integer
reg/clr		P0
func/call	NULL, [settype], [psInteger]
local/jmp	&[.loop]

.flt
% Set number type to floating-point
reg/clr		P0
func/call	NULL, [settype], [psFloat]
local/jmp	&[.loop]

.rat
% Set number type to rational
reg/clr		P0
func/call	NULL, [settype], [psRational]
local/jmp	&[.loop]

You will now be able to launch the calculator and run the new commands, although they won't do much for you at this time:

pscalc: a simple arbitrary-precision calculator
Type 'help' for a list of commands or 'quit' to exit
> int
Number type: psInteger
> flt
Number type: psFloat
> rat
Number type: psRational
> quit

Step 4 - Displaying unrecognised commands

The next thing we will do is display any input that isn't a command. This provides a simple echo mechanism, so that arbitrary text can be included in the calculator output stream. To do this, we'll have a function called display() that takes one parameter - a string to display. The function definition to include in the ._init section looks like this:

func/def	[display],  &[.func_display],	NULL, P0, [s]

The function returns no value, but accepts one parameter which will be called s and will contain a string.

The function code is as follows:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% display()
%
% Displays string to stdout (appending a newline character)
%
% Arguments:
%	s = string to display
%
% Returns:
%	None
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.func_display
var/addr	P0, [s]
attr/copy	P0, P0, [psString]
reg/copy	P0, P0, [\n]
attr/mod	![.prose.sys.io], [psStreamOut], P0
func/rtn

To make use of the display() routine, replace the .cmdloop_end section with the following code:

.cmdloop_end
reg/clr		P1
func/call	NULL, [display], P0
local/jmp	&[.loop]

We no longer need to reg/clr P0 because we are passing the string to the function. When you have a register containing a PSUNIT_TYPE_STRING or PSUNIT_TYPE_XVALUE and you pass it to a function as an argument, the register will be cleared and you will no longer have access to that pointer. This is because the data pointer is automatically transferred to the attribute value of the function's new local variable object in the same way as if attr/add or attr/mod had been used.

Now when the program is run, any arbitrary text that is not a recognised command will be echoed back to standard output. Here is an example:

pscalc: a simple arbitrary-precision calculator
Type 'help' for a list of commands or 'quit' to exit
> 159
159
> 13+209
13+209
> quit

At this time the add operation above is not recognised, so the sum is just echoed back to the screen.

Step 5 - Adding a debug mode

The debug command will enable and disable debug mode. When enabled, extra debug output will be displayed. To support this feature, first we'll have a global variable called debug that will have the type psIndex. This variable type is used for 32-bit unsigned integers. It exists primarily as a means of expressing the PSUNIT_TYPE_RAWIDX register in variable form, but can be used for any purpose where 32-bits are sufficient for the task at hand.

Start by creating the global variable at the .main label but before .loop. The section should now look like this:

.main
func/call	NULL, [title]
var/global	NULL, [psIndex], [debug], #0
var/global	NULL, [psString], [numtype], [psInteger]

.loop

Then we need a couple new functions. One is used to toggle debug mode on and off, and the other can be used to query whether debug mode is enabled.

The function definitions should go in the ._init section:

func/def	[toggle_debug], &[.func_debug]
func/def	[is_debug], &[.func_is_debug],	P2

The toggle_debug() function will return no value and accept no parameters. The is_debug() function returns a psBoolean type, which will be FALSE (or #0) to indicate that debug mode is off, or TRUE (or #1) to indicate that it is on.

The function code can be added to the end of the program:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% debug()
%
% Toggle debug mode ON or OFF
%
% Arguments:
%	None
%
% Returns:
%	None
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.func_debug
var/addr	P0, [debug]
attr/index	A, P0, [psIndex]
opa/xor		#1
attr/mod	P0, [psIndex], A
reg/jmpeq	&[.debug_on], A, #1

.debug_off
reg/load	P0, [OFF]
local/jmp	&[.debug_status]

.debug_on
reg/load	P0, [ON]

.debug_status
% Report debug status to stderr
reg/copy	P0, [Debug mode ], P0, [\n]
attr/mod	![.prose.sys.io], [psStreamError], P0
func/rtn

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% is_debug()
%
% Tests to see if debug mode is ON or OFF
%
% Arguments:
%	None
%
% Returns:
%	#0 if debug mode if OFF, #1 if it is ON
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.func_is_debug
var/addr	P0, [debug]
attr/index	A, P0, [psIndex]
func/rtn	A

In .func_debug the var/addr instruction is used to obtain a pointer to the debug global variable. Then we use attr/index to read the psIndex attribute value and convert it into a raw index (PSUNIT_TYPE_RAWIDX), where it is stored in the Accumulator and an exclusive OR operation performed to toggle the status between on and off.

Now the debug command needs to be added to the cmdlist data segment:

EQUP { &[.debug] }; EQUS { [debug] }

And we need to provide a .debug label, which can be inserted after .quit has returned and immediately before .help:

.debug
% Toggle debug mode ON or OFF
reg/clr		P0
func/call	NULL, [toggle_debug]
var/addr	P0, [debug]
attr/index	P0, P0, [psIndex]
reg/jmpeq	&[.debug_level_0], P0, #0
debug/level	#90
local/jmp	&[.loop]

.debug_level_0
debug/level	#0
local/jmp	&[.loop]

We do more than just call the debug() function here. We also use the debug/level instruction to set the prose engine debug level to 90 when debug mode is enabled. If prose was configured with the --enable-extradebug option before it was compiled, then debug level 90 reports when each function is entered and returned from. Here is such example output once the above changes have been made:

pscalc: a simple arbitrary-precision calculator
Type 'help' for a list of commands or 'quit' to exit
> debug
Debug mode ON
<0> [rt_support.c:652] Engine_setdebug() { DEBUG LEVEL CHANGING 90-90 }
<90> .prose.code.default.getcmd() called
> flt
<90> .prose.code.default.getcmd() returning
<90> .prose.code.default.settype() called
Number type: psFloat
<90> .prose.code.default.settype() returning
<90> .prose.code.default.getcmd() called
> 3+1
<90> .prose.code.default.getcmd() returning
<90> .prose.code.default.display() called
3+1
<90> .prose.code.default.display() returning
<90> .prose.code.default.getcmd() called
> quit
<90> .prose.code.default.getcmd() returning
<90> .prose.code.default.main() returning
<0> [rt_support.c:652] Engine_setdebug() { DEBUG LEVEL CHANGING 0-0 }

Don't worry if you did not compile prose with --enable-extradebug. In this case the debug mode we've added to pscalc won't be quite as verbose, but we'll use it in the next step.

Step 6 - Adding an rsplit() routine

Maths commands will all take two operands. A left numeric component, the operator (such as + or -) and the right numeric component. Each numeric component will be expected to be provided in the format suitable for the number type pre-selected by the int, flt or rat commands we have already built into our program.

We will be able to use the reg/xscan instruction to find the operator, and we can use reg/save to shorten the length of a string thereby extracting the left numeric component. However, extracting the right numeric component is not so straightforward, so we will add a function to do that for us.

First we will define it in the ._init section:

func/def	[rsplit],   &[.func_rsplit],	P0,   P0, [s], P1, [i]

The function will return a string, and it has two parameters. The first, s, will be the string that needs to be split. The second, i will be the index position where the split is to occur. Here is the function itself:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% rsplit()
%
% Splits a string at a given index position returning the portion to the
% right of the split
%
% Arguments:
%	s = string to split
%	i = index position of split
%
% Returns:
%	String
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.func_rsplit
var/addr	P0, [s], P1, [i]
attr/copy	P0, P0, [psString]
attr/xcopy	P1, P1, [psInteger]
reg/xload	P5, (P0)
op/incr		P1

% Set-up new string of the correct size
attr/def	P4, [psInteger], #0
opx/sub		P4, P5, P1
reg/copy	P3, []
reg/save	P3, (P4)

% Copy 2nd portion 32 bits at a time
attr/def	P4, [psInteger], #0

.rs_loop
reg/cmp		P1, P5
reg/jmpneq	&[.rs_end], SCMP, #2

reg/load	P2, (P0, P1)
reg/save	P3, (P2, P4)

% Move on 4 bytes and loop again
opx/add		P1, P1, #4
opx/add		P4, P4, #4
local/jmp	&[.rs_loop]

.rs_end
reg/clr		P0
func/rtn	P3

Step 7 - Adding the maths operations

All supported maths operations will be performed by a single section of function code. However, that code will be attached to four separate function names - add(), sub(), mult() and div() to perform addition, subtraction, multiplication and division respectively. This is because much of the code in the routine is identical regardless of the operation. We do this by defining the four separate function names but providing the same code pointer for each in the ._init section:

func/def	[add],	    &[.func_op],	P0,   P0, [s], P1, [i]
func/def	[sub],	    &[.func_op],	P0,   P0, [s], P1, [i]
func/def	[mult],	    &[.func_op],	P0,   P0, [s], P1, [i]
func/def	[div],	    &[.func_op],	P0,   P0, [s], P1, [i]

The section labelled .func_op provides the code for all of the functions. Inside the function we can determine the name the function was called with thereby ascertaining which operation is desired.

The routine will return the result as a string (so the caller can display the result more easily). It accepts two parameters: the string that needs to be processed (left numeric component, operator symbol, right numeric component) and the index position of the operator symbol:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% add(), sub(), mult(), div()
%
% Perform maths operation on two operands
%
% Arguments:
%	s = string in form M+N
%	i = index position of + symbol
%
% Returns:
%	Result (as a string)
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
.func_op
var/addr	P0, [s], P1, [i]

% Extra debug output if debug mode is ON
func/bcall	P10, [is_debug]
reg/jmpneq	&[.add_ops], P10, [1]
obj/dump	PCTX, P0, P1

.add_ops
% Read variable values
attr/copy	P0, P0, [psString]
attr/xcopy	P1, P1, [psInteger]
attr/def	PUSH, [psInteger], P1

% Split into two operands
reg/copy	P2, P0
func/bcall	P3, [rsplit], P2, P1
stack/pull	P1
reg/save	P0, (P1)
reg/clr		P10
reg/copy	P1, P3
reg/clr		P3

% Operands are in P0 and P1
% Get number type and perform add operation
var/addr	P4, [numtype]
attr/copy	P4, P4, [psString]
error/jmp	&[.bad_number1], ![.prose.error.sys.BadNumber]
var/local	P0, P4, [m], P0
error/jmp
error/jmp	&[.bad_number2], ![.prose.error.sys.BadNumber]
var/local	P1, P4, [n], P1

% Get name of function, this determines the action we perform
attr/copy	P2, PCTX, [pn]
reg/cmp		P2, [add], P2, [sub], P2, [mult], P2, [div]
reg/clr		P2
reg/jmpeq	&[.do_div], SFLG, #1
reg/jmpeq	&[.do_mult], SFLG, #2
reg/jmpeq	&[.do_sub], SFLG, #4

.do_add
opo/add		P0, P0, P1
local/jmp	&[.do_return]

.do_sub
opo/sub		P0, P0, P1
local/jmp	&[.do_return]

.do_mult
opo/mult	P0, P0, P1
local/jmp	&[.do_return]

.do_div
opo/div		P0, P0, P1

% Return result
.do_return
attr/xcopy	P0, P0, P4
reg/clr		P4
func/rtn	P0

.bad_number1
reg/load	P15, #1
local/jmp	&[.bad_number]

.bad_number2
reg/load	P15, #2

.bad_number
reg/clr		P0, P1
reg/copy	P0, [Bad number for selected number type ],
    			[(operand ], P15, [ is not a ], P4, [)]
error/now	![.prose.error.sys.BadNumber], P0

This function splits the string at the operator, stores the left and right numeric components in two variables called m and n and then uses the appropriate opo instruction to add, subtract, multiply or divide. The opo suite of maths instructions in PAL operate on variable objects. There are equivalent routines for operating on XVALUEs (opx) as well as on raw indices (op) and the Accumulator (opa). See the op_maths(5), opa_maths(5), opo_maths(5) and opx_maths(5) man pages for further reference.

Now all that remains is to insert the code that recognises a maths operation and calls the appropriate function. So we replace all code we've added so far from .cmdloop_end down to our first local/jmp &[.loop] with the following:

.cmdloop_end
%
% Process a mathematical command
%
reg/clr		P1
reg/copy	P2, P0
reg/xscan	P1, P2, [+]
reg/jmpneq	&[.add], P1, NULL
reg/xscan	P1, P2, [-]
reg/jmpneq	&[.sub], P1, NULL
reg/xscan	P1, P2, [*]
reg/jmpneq	&[.mult], P1, NULL

%
% The divide symbol can be / unless in psRational mode
%
var/addr	P15, [numtype]
attr/copy	P14, P15, [psString]
reg/cmp		P14, [psRational]
reg/clr		P14
reg/jmpneq	&[.div_slash], SCMP, #1
reg/xscan	P1, P2, [~]
reg/jmpneq	&[.div], P1, NULL
local/jmp	&[.display]

.div_slash
reg/xscan	P1, P2, [/]
reg/jmpneq	&[.div], P1, NULL

%
% Display anything else verbatim to stdout
%
.display
reg/clr		P2
func/call	NULL, [display], P0
local/jmp	&[.loop]

We also need .add, .sub, .mult and .div labels which effectively just call the appropriate function. Add these before .help as follows:

.add
% Add two operands and display result
func/bcall	P3, [add], P0, P1
reg/move	P0, P3
local/jmp	&[.display]

.sub
% Subtract two operands and display result
func/bcall	P3, [sub], P0, P1
reg/move	P0, P3
local/jmp	&[.display]

.mult
% Multiply two operands and display result
func/bcall	P3, [mult], P0, P1
reg/move	P0, P3
local/jmp	&[.display]

.div
% Divide two operands and display result
func/bcall	P3, [div], P0, P1
reg/move	P0, P3
local/jmp	&[.display]

Resources from this tutorial

Further reading

See the other tutorials available for the PROSE Assembly Language on the tutorials index page.

PROSE is released with detailed manual pages that describe how PAL operates, and how each instruction is used. These manual pages can be read using the man command, for example man pal_intro or man pal_commands, or from the Reference Manual Pages online.