Short tour of PAL
Contents
Short tour of PAL
In the current release, only the PROSE Assembly Language (PAL) is available. 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 tour.
This article gives a brief tour of the following features in the PROSE Assembly Language.
Registers and indices
There are 177 unique instruction codes (opcodes) in PAL, 26 registers and 6 data entry macros. Run prism -l and you'll see a list of them all:
$ prism -l
------------------------------------------------------------
INSTRUCTIONS
0x80 noop
0x81 stack/push
0x82 stack/pull
0x84 stack/lock
0x85 stack/unlock
0x86 stack/flush
0x88 obj/cmp
0x89 obj/def
0x8921 obj/clone
0x8922 obj/edit
0x8a obj/commit
0x8a21 obj/commitref
0x8b obj/del
0x8b21 tree/del
0x8c obj/addr
0x8c21 obj/pa
0x8c22 obj/child
0x8d obj/dump
0x8e obj/refresh
0x92 class/add
0x9221 class/del
0x93 class/load
0x9321 attr/load
0x94 class/load()
0x9421 attr/load()
0x95 class/test
0x97 attr/add
0x9721 attr/del
0x9722 attr/mod
0x98 attr/mvadd
0x9821 attr/mvdel
0x9822 attr/mvmod
0x99 attr/test
0x9921 attr/mvtest
0x9a attr/copy
0x9a21 attr/copy()
0x9a22 attr/index
0x9a23 attr/addr
0x9a24 attr/xcopy
0x9a25 attr/xcopy()
0x9b attr/direct
0x9b21 attr/cmp
0x9c attr/def
0x9d attr/xconv
0xa6 op/incr
0xa7 op/decr
0xa8 op/add
0xa821 opo/add
0xa822 opx/add
0xa9 op/sub
0xa921 opo/sub
0xa922 opx/sub
0xaa op/mult
0xaa21 opo/mult
0xaa22 opx/mult
0xab op/div
0xab21 opo/div
0xab22 opx/div
0xac op/mod
0xac21 opo/mod
0xac22 opx/mod
0xad op/not
0xad21 opo/not
0xad22 opx/not
0xae op/and
0xae21 opo/and
0xae22 opx/and
0xaf op/or
0xaf21 opo/or
0xaf22 opx/or
0xb0 op/xor
0xb021 opo/xor
0xb022 opx/xor
0xb1 op/shl
0xb121 opo/shl
0xb122 opx/shl
0xb2 op/shr
0xb221 opo/shr
0xb222 opx/shr
0xb3 op/mask
0xb321 op/swap
0xb5 list/def
0xbb error/def
0xbc error/now
0xbd error/jmp
0xbe error/clr
0xbf func/addr
0xbf21 var/addr
0xc0 func/def
0xc1 func/call
0xc121 func/bcall
0xc2 func/rtn
0xc4 local/jmp()
0xc421 local/jsr()
0xc5 local/jmp
0xc521 local/jsr
0xc6 local/rtn
0xcc reg/load
0xcc21 reg/move
0xcd reg/jmpeq
0xcd21 reg/jmpneq
0xcd22 reg/jmplt
0xcd23 reg/jmple
0xcd24 reg/jmpgt
0xcd25 reg/jmpge
0xce reg/jsreq
0xce21 reg/jsrneq
0xce22 reg/jsrlt
0xce23 reg/jsrle
0xce24 reg/jsrgt
0xce25 reg/jsrge
0xcf reg/dump
0xd0 reg/load()
0xd021 reg/xload()
0xd1 reg/clr
0xd2 reg/index
0xd3 reg/cmp
0xd4 reg/save()
0xd5 reg/copy
0xd521 reg/copy()
0xd522 reg/conv
0xd6 reg/lcmp
0xd621 reg/rcmp
0xd7 reg/xscan
0xd8 reg/type
0xd9 reg/roll
0xe0 var/def
0xe021 mtx/def
0xe022 itree/def
0xe023 itree/bdef
0xe024 xtree/def
0xe025 xtree/bdef
0xe1 var/local
0xe121 mtx/local
0xe122 itree/local
0xe123 itree/blocal
0xe124 xtree/local
0xe125 xtree/blocal
0xe2 var/static
0xe221 mtx/static
0xe222 itree/static
0xe223 itree/bstatic
0xe224 xtree/static
0xe225 xtree/bstatic
0xe3 var/global
0xe321 mtx/global
0xe322 itree/global
0xe323 itree/bglobal
0xe324 xtree/global
0xe325 xtree/bglobal
0xe4 tree/clone
0xe421 tree/bclone
0xe8 opa/add
0xe9 opa/sub
0xea opa/mult
0xeb opa/div
0xec opa/mod
0xed opa/not
0xee opa/and
0xef opa/or
0xf0 opa/xor
0xf1 opa/shl
0xf2 opa/shr
0xf3 mtx/size
0xf321 mtx/bsize
0xf322 mtx/dim
0xf4 mtx/resize
0xf421 mtx/bresize
0xf5 mtx/set
0xf521 itree/addr
0xf522 xtree/addr
0xf523 itree/set
0xf524 xtree/set
0xf6 itree/xconv
0xf621 xtree/iconv
0xfb debug/source
0xfc debug/level
------------------------------------------------------------
INDICES
0x40 to 0x43 IDESCTYPE_LABEL
0x44 to 0x47 IDESCTYPE_DLABEL
0x48 to 0x4b IDESCTYPE_OBJREF
0x4c to 0x4f IDESCTYPE_TEXT
0x50 to 0x53 IDESCTYPE_RAW
0x5c ------- IFLAG_PTRPOS
0x5d ------- IFLAG_PTRNEG
0x60 to 0x63 LISTIDX_POS
0x64 to 0x67 LISTIDX_NEG
0x68 to 0x6b LISTIDX_MPOS
0x6c to 0x6f LISTIDX_MNEG
0x7c ------- LXFLAG_STATE
------------------------------------------------------------
REGISTERS
0x00 P0
0x01 P1
0x02 P2
0x03 P3
0x04 P4
0x05 P5
0x06 P6
0x07 P7
0x08 P8
0x09 P9
0x0a P10
0x0b P11
0x0c P12
0x0d P13
0x0e P14
0x0f P15
0x16 PERR
0x17 A
0x18 SFLG
0x19 SCMP
0x1a LOCK
0x1b PCTX
0x1c PEEK
0x1d PULL
0x1e PUSH
0x1f NULL
------------------------------------------------------------
MACROS
0x20 EQUB
0x40 EQUW
0x60 EQUD
0x80 EQUS
0xa0 EQUP
0xc0 EQUI
Registers are pointers to units of data. They can point to text strings, nodes (objects in the nexus), program code, and a variety of other data types. The type of data is always associated with the register, so that bytecode instructions can validate, to a certain degree, that the arguments they have been given are correct.
Besides registers, other valid arguments that may be passed to PAL instructions are called indices. An index parameter is so called because when it is compiled into bytecode, it is saved to a table, and the argument itself converted to a numeric index. There are 12 types of index, although only 9 of these can be used explicitly within a PAL source file. Indices allow for data such as arbitrary text, code references and object references to be input.
The following code example demonstrates the use of registers and indices. It loads text and code pointers into a number of registers, and then dumps the contents to the screen.
% This program will only have an _init section for now
._init
% Load registers P0 and P1
reg/load P0, [A violin concerto], P1, [Brahms]
% Load registers P10 and P11
reg/load P10, &[._init], P11, [Brahms]
% Report contents of registers
reg/dump P0, P1, P10, P11
obj/dump P0, P1, P10, P11
Compile and run the code:
$ prism test
$ prose test
register: P0
type: PSUNIT_TYPE_PALTEXT (0x07)
0x7f3ca1e3c048 (len 0x000011)
register: P1
type: PSUNIT_TYPE_PALTEXT (0x07)
0x7f3ca1e3c05b (len 0x000006)
register: P10
type: PSUNIT_TYPE_PALCODE (0x05)
0x0000
register: P11
type: PSUNIT_TYPE_PALTEXT (0x07)
0x7f3ca1e3c05b (len 0x000006)
0x7f3ca1e3c048 (len 0x000011)
A violin concerto
0x7f3ca1e3c05b (len 0x000006)
Brahms
0x7f3ca1e3c05b (len 0x000006)
Brahms
prose: ERROR: no main() function found, nothing to do
The error 'no main() function found' means that the PROSE engine could only call the _init section and there was no main function from which to begin full program execution. For the purposes of this exercise the error can be ignored. For simplicity we have omitted defining a main function (as functions are described later on in this tour).
If you run prism -v on the binary, you'll see that the text 'A violin concerto' and 'Brahms' were given the index numbers 0 and 1 respectively. Note that even though the text 'Brahms' appeared twice in the source file, it only has one entry in the text table.
$ prism -v test
------------------------------------------------------------
PROSE HEADER
File: test.pro
Size: 109 bytes
Compiled by: prism
Compiler version code: 0.11.0
Compile date: Fri Sep 22 17:28:45 2017
------------------------------------------------------------
INSTRUCTION CODE
000000 : cc 00 4c 00 01 4c 01 cc 0a 5d 09 0b 4c 01 cf 00
000010 : 01 0a 0b 8d 00 01 0a 0b
Size: 24 bytes
------------------------------------------------------------
CODE LABELS
idx 000000 len 000005 [_init]
Size: 7 bytes
------------------------------------------------------------
CODE ADDRESSES
idx 000000 ref 000000
Size: 1 bytes
------------------------------------------------------------
TEXT DATA
idx 000000 len 000011 [A violin concerto]
idx 000001 len 000006 [Brahms]
Size: 27 bytes
------------------------------------------------------------
DATA LABELS
Size: 0 bytes
------------------------------------------------------------
DATA XREF TABLE
Size: 0 bytes
------------------------------------------------------------
DATA SEGMENTS
Size: 0 bytes
------------------------------------------------------------
END OF FILE
How to use the stack
The program stack is sized dymamically up and down as items are pushed onto it, and pulled off it. There are no theoretical limits to the number of items that can be pushed onto the stack, as long as there is available system memory.
Data is pushed onto the stack either using specific instructions that manipulate it, or by using special register keywords such as PUSH, PULL and PEEK.
The stack may also be locked. This doesn't prevent data from being pushed onto the stack, but it does protect the data already on the stack from being pulled off it. A lock may only be removed using the stack/unlock instruction, when the topmost item on the stack is the lock to be removed.
The following demonstrates stack operations:
% This program will only have an _init section for now
._init
% Load registers P0 and P1 and push onto stack
reg/load P0, [A violin concerto], P1, [Brahms]
stack/push P0, P1
% Push code pointer onto stack
reg/load PUSH, &[._init]
% Pull items off stack
reg/load P10, PULL
reg/dump PULL, PULL
reg/dump P10
When run, the above code yields the following output:
register: PULL
type: PSUNIT_TYPE_PALTEXT (0x07)
0x2b8eb7de505c (len 0x000006)
register: PULL
type: PSUNIT_TYPE_PALTEXT (0x07)
0x2b8eb7de5049 (len 0x000011)
register: P10
type: PSUNIT_TYPE_PALCODE (0x05)
0x0000
prose: ERROR: no main() function found, nothing to do
Local branching
Without code sections or the ability to branch, the program pointer would only be able to move from one instruction to the next during bytecode execution, therefore the instructions would always be read in the order in which they appear in the source file.
PAL provides several mechanisms for modifying the program pointer. Branching is used to describe the ability to jump around to different sections of the local file. If you want to jump to a section of code in a different file, you need to use functions, which are described a little later on in this tour.
To branch to a code label, use either the local/jsr instruction which executes a subroutine that should be returned from using local/rtn, or use the local/jmp instruction which you cannot return from. These are demonstrated below:
% This program will only have an _init section for now
._init
local/jsr &[.routine1]
local/jmp &[.routine2]
% This code section will return to the caller
.routine1
obj/dump [routine1]
local/rtn
% This code section will exit
.routine2
obj/dump [routine2]
local/rtn
Conditional branching allows sections of code within the local file to be called only if a certain condition is met. Registers can be tested using the reg/jmpeq, reg/jsreq, reg/jmpneq and reg/jsrneq instructions. These instructions operate like their local/jmp and local/jsr cousins, but only if the two arguments are equal (eq) or not equal (neq).
% This program will only have an _init section for now
._init
reg/load P0, [Mozart]
reg/load P1, [Beethoven]
reg/jsreq &[.equal], P0, [Mozart]
reg/jmpneq &[.notequal], P1, [Mozart]
.equal
obj/dump [register comparison equal]
local/rtn
.notequal
obj/dump [register comparison not equal]
local/rtn
There is also
reg/jmplt,
reg/jsrlt,
reg/jmple,
reg/jsrle,
reg/jmpgt,
reg/jsrgt,
reg/jmpge and
reg/jsrge
that only branch if their two arguments are less than (lt), less than or equal to (le), greater than (gt) or greater than or equal to (ge).
Register comparison
Other types of comparison may be performed by using specific comparison instructions such as reg/cmp. These affect the value of the SCMP and SFLG registers.
The SCMP register holds the result of the last test, which can be one of:
- 0 - Not equal
- 1 - Equal
- 2 - Less than
- 4 - Greater than
The SFLG register holds the equality status of the last 32 tests in successive bits. It is a 32-bit integer where the lowest bit (bit 0) is set if the last test was equal, the next bit (bit 1) is set if the test before that was equal and so on.
The reg/clr instruction can be used for clearing the SFLG and SCMP registers.
The following code demonstrates using these registers:
% This program will only have an _init section for now
._init
reg/load P0, #59, P1, #72
reg/cmp P0, P1
reg/jsreq &[.lt], SCMP, #2
reg/jsreq &[.gt], SCMP, #4
reg/clr
reg/load P2, P0, P4, P2, P3, P1, P5, P3
reg/cmp P0, P2, P1, P3, P0, P4, P1, P5
reg/jmpeq &[.equal], SFLG, #15
obj/dump [second test - not all comparisons were equal]
local/rtn
.lt
obj/dump [first test - result: less than]
local/rtn
.gt
obj/dump [first test - result: greater than]
local/rtn
.equal
obj/dump [second test - all comparisons equal]
Data segments
Arbitrary data can be entered into a PAL source file using data entry macros. This data is stored within sections of the bytecode file called data segments, which can be processed using the reg/load instruction in one of its indirect addressing modes. Indirect addressing is enabled if one or more of the instruction arguments are enclosed in parentheses. Different argument types enable different functionality with some of the PAL instructions when indirect addressing is used. It is a powerful feature that is described in length in the online manual pages, see pal_macros(5).
Assemble the following example:
~strings
EQUS {[first violins], [second violins]}
EQUS {[violas], [cellos], [bass]}
~woodwind
EQUS {[piccolos], [flutes], [clarinets], [oboes]}
~odd_numbers
EQUB {1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
EQUB {21, 23, 25, 27, 29, 31, 33, 35, 37, 39}
EQUD {0x292b2d2f, 0x31333537}
~pointers
EQUP {&[.jump1], &[.jump2], &[.jump3], &[.jump4]}
~segments
EQUP {&[~strings], &[~woodwind]}
EQUP {&[~odd_numbers], &[~pointers]}
._init
% Process each data segment listed in ~segments
reg/load P0, (&[~segments])
.loop
reg/load P1, (P0)
reg/jmpeq &[.stop], P1, NULL
% Now read each item from the selected data segment
reg/load P2, (P1)
.loop2
reg/load P3, (P2)
reg/jmpeq &[.next], P3, NULL
obj/dump P3
% This might be a code pointer, so try calling it
% If it isn't a code pointer, we discard the error
error/jmp &[.trap]
local/jsr P3
.trap
error/clr
error/jmp
local/jmp &[.loop2]
.next
reg/clr P2
local/jmp &[.loop]
.stop
local/rtn
.jump1
obj/dump [jump1 called]
local/rtn
.jump2
obj/dump [jump2 called]
local/rtn
.jump3
obj/dump [jump3 called]
local/rtn
.jump4
obj/dump [jump4 called]
local/rtn
When executed, the output should look like this:
0x2b961ac240b0 (len 0x00000d)
first violins
0x2b961ac240bf (len 0x00000e)
second violins
0x2b961ac240cf (len 0x000006)
violas
0x2b961ac240d7 (len 0x000006)
cellos
0x2b961ac240df (len 0x000004)
bass
0x2b961ac240e5 (len 0x000008)
piccolos
0x2b961ac240ef (len 0x000006)
flutes
0x2b961ac240f7 (len 0x000009)
clarinets
0x2b961ac24102 (len 0x000005)
oboes
0x1
0x3
0x5
0x7
0x9
0xb
0xd
0xf
0x11
0x13
0x15
0x17
0x19
0x1b
0x1d
0x1f
0x21
0x23
0x25
0x27
0x292b2d2f
0x31333537
0x2b961ac24109 (len 0x00000c)
jump1 called
0x2b961ac24117 (len 0x00000c)
jump2 called
0x2b961ac24125 (len 0x00000c)
jump3 called
0x2b961ac24133 (len 0x00000c)
jump4 called
prose: ERROR: no main() function found, nothing to do
You can see how the data segments are stored in the bytecode when you run prism -v, as in the following example:
$ prism -v test
------------------------------------------------------------
PROSE HEADER
File: test.pro
Size: 453 bytes
Compiled by: prism
Compiler version code: 0.11.0
Compile date: Mon Sep 25 07:15:23 2017
------------------------------------------------------------
INSTRUCTION CODE
000000 : d0 00 44 04 d0 01 00 cd 5c 21 01 1f d0 02 01 d0
000010 : 03 02 cd 5c 11 03 1f 8d 03 bd 5c 05 c5 21 03 be
000020 : bd c5 5d 13 d1 02 c5 5d 23 c6 8d 4c 09 c6 8d 4c
000030 : 0a c6 8d 4c 0b c6 8d 4c 0c c6
Size: 58 bytes
------------------------------------------------------------
CODE LABELS
idx 000000 len 000005 [jump1]
idx 000001 len 000005 [jump2]
idx 000002 len 000005 [jump3]
idx 000003 len 000005 [jump4]
idx 000004 len 000005 [_init]
idx 000005 len 000004 [loop]
idx 000006 len 000004 [stop]
idx 000007 len 000005 [loop2]
idx 000008 len 000004 [next]
idx 000009 len 000004 [trap]
Size: 66 bytes
------------------------------------------------------------
CODE ADDRESSES
idx 000000 ref 00002a
idx 000001 ref 00002e
idx 000002 ref 000032
idx 000003 ref 000036
idx 000004 ref 000000
idx 000005 ref 000004
idx 000006 ref 000029
idx 000007 ref 00000f
idx 000008 ref 000024
idx 000009 ref 00001f
Size: 10 bytes
------------------------------------------------------------
TEXT DATA
idx 000000 len 00000d [first violins]
idx 000001 len 00000e [second violins]
idx 000002 len 000006 [violas]
idx 000003 len 000006 [cellos]
idx 000004 len 000004 [bass]
idx 000005 len 000008 [piccolos]
idx 000006 len 000006 [flutes]
idx 000007 len 000009 [clarinets]
idx 000008 len 000005 [oboes]
idx 000009 len 00000c [jump1 called]
idx 00000a len 00000c [jump2 called]
idx 00000b len 00000c [jump3 called]
idx 00000c len 00000c [jump4 called]
Size: 145 bytes
------------------------------------------------------------
DATA LABELS
idx 000000 len 000007 [strings]
idx 000001 len 000008 [woodwind]
idx 000002 len 00000b [odd_numbers]
idx 000003 len 000008 [pointers]
idx 000004 len 000008 [segments]
Size: 52 bytes
------------------------------------------------------------
DATA XREF TABLE
idx 000000 ref 000000
idx 000001 ref 000001
idx 000002 ref 000002
idx 000003 ref 000003
idx 000004 ref 000004
Size: 5 bytes
------------------------------------------------------------
DATA SEGMENTS
idx 000000 len 000007 {
81 80 81 82 82 83 84
}
idx 000001 len 000005 {
83 85 86 87 88
}
idx 000002 len 00001f {
29 01 03 05 07 09 0b 0d 0f 11 13 29 15 17 19 1b
1d 1f 21 23 25 27 61 29 2b 2d 2f 31 33 35 37
}
idx 000003 len 000009 {
a3 40 00 40 01 40 02 40 03
}
idx 000004 len 00000a {
a1 44 00 44 01 a1 44 02 44 03
}
Size: 67 bytes
------------------------------------------------------------
END OF FILE
Note that a record is kept in the bytecode of which data entry macros were used to create the data segments. That way the data segments will return the same data items to reg/load(), and they will also appear the same as the source file when disassembled with prism -d.
Functions
When a program is first loaded into the PROSE engine, it is represented by a single object that is attached to the nexus in a particular location (the module root). All functions that are provided by that program must be created using the func/def instruction, usually within the _init section. Each function that is created this way is then represented in the nexus by its own object, attached underneath the module root.
Once the _init section has completed, PROSE will search for a function called main, which should have been created underneath the module root. If it doesn't exist, the following error is displayed, which you will already be familiar with:
prose: ERROR: no main() function found, nothing to do
The following example sets up the required main function, and then displays details about the main object once the function is executed. This will display the location of the main object within the nexus hierarchy, and the classes and attributes assigned to that object.
._init
func/def [main], &[.main]
local/rtn
.main
obj/dump
func/rtn
Here is the output when the above example is run:
.prose.code.default.main:objectClass=psContainer
.prose.code.default.main:objectClass=top
.prose.code.default.main:objectClass=psFunction
.prose.code.default.main:pn=[main]
.prose.code.default.main:psObjectFileRd=[test.pro]
.prose.code.default.main:psObjectCodeRef=[0x0006]
.prose.code.default.main:psModuleRoot=[.prose.code.default]
Note that the correct way of returning from a function is by use of the func/rtn instruction, not local/rtn which is reserved for completing the _init section or returning from local branches.
A function is called using func/call or func/bcall. When a function is called, all registers will be zeroed, including SCMP, SFLG and PERR. When a function returns, if it was called with func/bcall then all non-zero registers will be restored to their original value, otherwise they will be zeroed again. It is known as func/bcall because this instruction bundles up the registers and pushes them onto the stack before executing the function, restoring the bundle from the stack upon return. This process is less efficient, so try to use func/call as your default choice, and only use func/bcall when you need the registers to be restored.
The difference between func/call and func/bcall is demonstrated below:
._init
func/def [main], &[.main]
func/def [func1], &[.my_function]
local/rtn
.main
reg/load P0, [A piano duet]
reg/load P13, P0
reg/load SCMP, #1
func/bcall NULL, [func1]
reg/dump P0, P13, SCMP
func/call NULL, [func1]
reg/dump P0, P13, SCMP
func/rtn
.my_function
obj/dump
func/rtn
This produces:
.prose.code.default.func1:objectClass=psContainer
.prose.code.default.func1:objectClass=top
.prose.code.default.func1:objectClass=psFunction
.prose.code.default.func1:pn=[func1]
.prose.code.default.func1:psObjectFileRd=[test.pro]
.prose.code.default.func1:psObjectCodeRef=[0x0028]
.prose.code.default.func1:psModuleRoot=[.prose.code.default]
register: P0
type: PSUNIT_TYPE_PALTEXT (0x07)
0x7f1d333bf07c (len 0x00000c)
register: P13
type: PSUNIT_TYPE_PALTEXT (0x07)
0x7f1d333bf07c (len 0x00000c)
register: SCMP
type: PSUNIT_TYPE_RAWIDX (0x08)
0x1 (PSCMP_EQUAL)
.prose.code.default.func1:objectClass=psContainer
.prose.code.default.func1:objectClass=top
.prose.code.default.func1:objectClass=psFunction
.prose.code.default.func1:pn=[func1]
.prose.code.default.func1:psObjectFileRd=[test.pro]
.prose.code.default.func1:psObjectCodeRef=[0x0028]
.prose.code.default.func1:psModuleRoot=[.prose.code.default]
register: P0
type: PSUNIT_TYPE_NULL (0x00)
register: P13
type: PSUNIT_TYPE_NULL (0x00)
register: SCMP
type: PSUNIT_TYPE_RAWIDX (0x08)
0x0 (PSCMP_NOTEQUAL)
Functions may also be created in subcontainers that are not directly underneath the module root. For example:
._init
func/def [main], &[.main]
func/def [tools.tuning.key], &[.tuning_key]
local/rtn
.main
obj/dump
func/call NULL, [tools.tuning.key]
func/rtn
.tuning_key
obj/dump
func/rtn
The example above yields the following output:
.prose.code.default.main:objectClass=psContainer
.prose.code.default.main:objectClass=top
.prose.code.default.main:objectClass=psFunction
.prose.code.default.main:pn=[main]
.prose.code.default.main:psObjectFileRd=[test.pro]
.prose.code.default.main:psObjectCodeRef=[0x000b]
.prose.code.default.main:psModuleRoot=[.prose.code.default]
.prose.code.default.tools.tuning.key:objectClass=psContainer
.prose.code.default.tools.tuning.key:objectClass=top
.prose.code.default.tools.tuning.key:objectClass=psFunction
.prose.code.default.tools.tuning.key:pn=[key]
.prose.code.default.tools.tuning.key:psObjectFileRd=[test.pro]
.prose.code.default.tools.tuning.key:psObjectCodeRef=[0x0011]
.prose.code.default.tools.tuning.key:psModuleRoot=[.prose.code.default]
It is possible to define variables and pass those variables to functions. It is also possible for functions to return variables. These are the subject of tutorials that can be located in PAL tutorials. In particular, see An arbitrary-precision calculator.
Loading multiple programs
It is possible to give PROSE a list of several files to load at start-up time. The _init section in each file will be run, which will set-up the necessary functions. Then the main function will be executed.
Each file may create functions in the same location in the nexus, i.e. the same module root, which would mean they cannot create functions with identical names. Alternatively they can deploy in different areas of the nexus, with their own namespace.
If the location of the module root is not specified in the PAL source file, the code will be loaded in the location .prose.code.default. Alternatively, a different location underneath .prose.code may be specified using a data segment with the special name Module.
The following example consists of three separate source files. Save this one as one.pal:
~Module
EQUS {[loft.util]}
._init
func/def [func1], &[.f1]
local/rtn
.f1
reg/dump PCTX
func/rtn
Here is two.pal:
~Module
EQUS {[loft.util]}
._init
func/def [func2], &[.f2]
local/rtn
.f2
reg/dump PCTX
func/rtn
And here is three.pal:
~Module
EQUS {[loft.test]}
._init
func/def [main], &[.main]
local/rtn
.main
reg/dump PCTX
reg/load PCTX, ![.prose.code.loft.util]
func/call NULL, ![func1]
func/call NULL, ![func2]
func/rtn
Now assemble and execute these programs:
$ prism -m one two three
$ prose one two three
register: PCTX
type: PSUNIT_TYPE_NODE (0x81)
root: 8096288 (global) nrefs: 4
.prose.code.loft.test.main
register: PCTX
type: PSUNIT_TYPE_NODE (0x81)
root: 8096288 (global) nrefs: 3
.prose.code.loft.util.func1
register: PCTX
type: PSUNIT_TYPE_NODE (0x81)
root: 8096288 (global) nrefs: 3
.prose.code.loft.util.func2
Generating and handling errors
When an error occurs, unless it is trapped by the program, PROSE will exit and display the details of the error with a stack trace. The stack trace tells you exactly where the error occurred, and the list of functions that had been called up to that point.
The following example defines a new error type, and generates an error. Save this as error.pal, assemble it and run it. Note that while this example opts for defining a new error type, it would also have been possible to use one of a number of pre-defined error types. Run man ps_errortypes for a full list.
~Module
EQUS {[loft.test]}
._init
func/def [main], &[.main]
func/def [func1], &[.f1]
local/rtn
.main
error/def [TestError], [Test error message]
func/call NULL, [func1]
func/rtn
.f1
error/now ![.prose.error.loft.test.TestError],
[This is a manually triggered error for testing purposes]
In the above example, the following stack trace will be displayed:
* prose.error.loft.test.TestError: Test error message
* This is a manually triggered error for testing purposes
* at loft.test.func1() [error.pro, addr 0x0015]
* at loft.test.main() [error.pro, addr 0x0010]
* in prose.code._tid.0
What this tells you is that the loft.test.main() function had called loft.test.func1() when the error occurred. Both functions are defined in the binary file error.pro, and the address reached within each function is displayed.
To understand the bytecode address, one needs to disassemble the bytecode with a special debug option as follows:
$ prism -D30 -d error
<< Instruction code: 0x00001c >>
<< Code labels: 0x00003a >>
<< Code addresses: 0x00004e >>
<< Text segment: 0x000055 >>
<< Data labels: 0x0000ea >>
<< Data addresses: 0x0000f5 >>
<< Data segment: 0x0000fa >>
~Module
EQUS { [loft.test] }
0x0000 : ._init
0x0000 : func/def [main], &[.main]
0x0005 : func/def [func1], &[.f1]
0x000a : local/rtn
0x000b : .main
0x000b : error/def [TestError], [Test error message]
0x0010 : func/call NULL, [func1]
0x0014 : func/rtn
0x0015 : .f1
0x0015 : error/now ![.prose.error.loft.test.TestError], [This is a manually triggered error for testing purposes]
In most real-world scenarios, once the PROSE scripting language is supported, the programmer will not be writing in assembly language, so making reference to addresses within bytecode files will make errors very difficult to trace. To circumvent this problem, the debug/source instruction can be used at intervals throughput PAL source files to instruct PROSE of the filename and line number of the originating source. Note that the source information is reset when a function is called, so at a minimum there must be one debug/source instruction per function, although usually you'll have many more than this.
The use of debug/source is demonstrated below, where a hypothetical script called hypothetical.ps has been compiled into these assembly instructions:
~Module
EQUS {[loft.test]}
._init
func/def [main], &[.main]
func/def [func1], &[.f1]
local/rtn
.main
debug/source [hypothetical.ps], #59
error/def [TestError], [Test error message]
debug/source #60
func/call NULL, [func1]
func/rtn
.f1
debug/source [hypothetical.ps], #88
error/now ![.prose.error.loft.test.TestError],
[This is a manually triggered error for testing purposes]
This yields the following stack trace:
* prose.error.loft.test.TestError: Test error message
* This is a manually triggered error for testing purposes
* at loft.test.func1() [hypothetical.ps, line 88]
* at loft.test.main() [hypothetical.ps, line 60]
* in prose.code._tid.0
When navigating a filesystem, you having a working directory, you have commands that allow you to move around from one directory to another, and commands for reading the contents of a directory. Many of the same features can be found in PAL for navigating around the nexus. The special register PCTX contains a pointer to the current context (akin to the working directory). Object reference indices may be used for accessing nodes that are relative to the current context or relative to the root of the nexus e.g. ![nodename], and walkers may be used for iterating nodes attached to a branch.
The following example demonstrates some basic navigation techniques:
~Module
EQUS {[loft.test]}
._init
func/def [main], &[.main]
local/rtn
.main
reg/load PCTX, ![.prose.error]
% Push first 5 system error definitions onto stack
.load
reg/load P0, (![sys])
reg/load PUSH, (P0)
reg/load PUSH, (P0)
reg/load PUSH, (P0)
reg/load PUSH, (P0)
reg/load PUSH, (P0)
reg/clr P0
% Then pull them off the stack, dumping their addresses
.unload
reg/dump PULL, PULL, PULL, PULL, PULL
func/rtn
The output of the above example is:
register: PULL
type: PSUNIT_TYPE_NODE (0x81)
root: 0x2208170 (global) nrefs: 1
.prose.error.sys.HiddenData
register: PULL
type: PSUNIT_TYPE_NODE (0x81)
root: 0x2208170 (global) nrefs: 1
.prose.error.sys.BadRegister
register: PULL
type: PSUNIT_TYPE_NODE (0x81)
root: 0x2208170 (global) nrefs: 1
.prose.error.sys.CircularReference
register: PULL
type: PSUNIT_TYPE_NODE (0x81)
root: 0x2208170 (global) nrefs: 1
.prose.error.sys.AlreadyExists
register: PULL
type: PSUNIT_TYPE_NODE (0x81)
root: 0x2208170 (global) nrefs: 1
.prose.error.sys.BadDevice
Combining walkers with the stack features of PAL, it is possible to write a short program that iterates every node in the nexus, dumping out its contents. This was demonstrated in the article Running your first program.
Further reading
This short tour of PAL briefly touched upon some of the features available to the low-level programmer. There are several important features that have not been included in this article including creating, deleting and modifying objects and variables in the nexus, basic input/output operations and passing variables between functions. These are the subject of dedicated tutorials that can be located in PAL tutorials.
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.