Short tour of PAL
Contents
Short tour of PAL in release 0.9.x
In release 0.9.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 tour.
This article gives a brief tour of the following features in the PROSE Assembly Language.
Registers and indices
There are 170 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 0x8b obj/del 0x8c obj/addr 0x8c21 obj/pa 0x8c22 obj/child 0x8d obj/dump 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 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() 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/scan 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 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 a _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) 0x2b82b788504b (len 0x000011) register: P1 type: PSUNIT_TYPE_PALTEXT (0x07) 0x2b82b788505e (len 0x000006) register: P10 type: PSUNIT_TYPE_PALCODE (0x05) 0x0000 register: P11 type: PSUNIT_TYPE_PALTEXT (0x07) 0x2b82b788505e (len 0x000006) 0x2b82b788504b (len 0x000011) A violin concerto 0x2b82b788505e (len 0x000006) Brahms 0x2b82b788505e (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.9.0 Compile date: Sat Mar 16 23:17:10 2013 ------------------------------------------------------------ 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 a _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 a _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 a _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
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 a _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.
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.9.0 Compile date: Sat Mar 16 23:20:37 2013 ------------------------------------------------------------ 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
It may seem unusual for a low-level assembly language to support functions, but because of their fundamental importance to the way the PROSE engine (and in particular the nexus) operates, you will find a number of features in PAL which ordinarily are only supported by higher-level languages. This makes PAL not only particularly unique, but also very efficient.
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=[0x0027] .prose.code.default.func1:psModuleRoot=[.prose.code.default] register: P0 type: PSUNIT_TYPE_PALTEXT (0x07) 0x2b6b36e2107c (len 0x00000c) register: P13 type: PSUNIT_TYPE_PALTEXT (0x07) 0x2b6b36e2107c (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=[0x0027] .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 one $ prism two $ prism three $ prose one two three register: PCTX type: PSUNIT_TYPE_NODE (0x01) .prose.code.loft.test.main register: PCTX type: PSUNIT_TYPE_NODE (0x01) .prose.code.loft.util.func1 register: PCTX type: PSUNIT_TYPE_NODE (0x01) .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: 0x00001f >> << Code labels: 0x00003d >> << Code addresses: 0x000051 >> << Text segment: 0x000058 >> << Data labels: 0x0000ed >> << Data addresses: 0x0000f8 >> << Data segment: 0x0000fd >> ~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 (0x01) .prose.error.sys.OutOfRange register: PULL type: PSUNIT_TYPE_NODE (0x01) .prose.error.sys.HiddenData register: PULL type: PSUNIT_TYPE_NODE (0x01) .prose.error.sys.BadRegister register: PULL type: PSUNIT_TYPE_NODE (0x01) .prose.error.sys.AlreadyExists register: PULL type: PSUNIT_TYPE_NODE (0x01) .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.