An arbitrary-precision calculator
Contents
PAL Tutorial - An arbitrary-precision calculator
In release 0.7.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. Note that this is based on GMP 4.3.2.
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 at least 64 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/load A, (P0) op/decr error/jmp &[.no_strip], ![.prose.error.sys.OutOfRange] reg/load P2, (P0, A) op/shr P2, P2, #24 reg/jmpneq &[.no_strip], P2, #10 reg/save P0, (A) 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, [psIndex] % % 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
. 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
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 project links on the main page of this wiki.