6. RPN compiler

The RPN compiler makes it possible to compile HP-41 RPN programs into the same output format as the Nut assembler. That is, it will output a relocatable object file and optionally a list file. The relocatable object file is suitable to be linked into a HP-41 module.

The RPN compiler is a command line tool named rpncomp. Running it from the command line with the ``–help`` option will give a sign-on message and display accepted options:

Calypsi RPN compiler for Hewlett-Packard Nut version 5.16

Usage: rpncomp [--version] [-o|--output-file OUTPUT-FILE] [-l]
               [--list-file LIST-FILE] [-I DIRECTORY]
               [--xeq-from-rom LABEL-LIST] [--rpn-output] [--raw-output]
               [--no-fat] [--prefix-labels PREFIX] FILE
  use 'rpncomp --help' for detailed help

Available options:
  --version                Display version number
  -o,--output-file OUTPUT-FILE
                           Name of output file
  -l                       Generate a list file, named by appending '.lst' to
                           input file
  --list-file LIST-FILE    Generate list file, using given name
  -I DIRECTORY             Include directory
  --xeq-from-rom LABEL-LIST
                           Specify XEQ global labels to keep as is (not change
                           to XROM calls, space separated list)
  --rpn-output             Expecting a .raw file, generate RPN source output
  --raw-output             generate .raw output
  --no-fat                 Omit auto generation of function address table entry
                           (fat)
  --prefix-labels PREFIX   Prefix created assembler labels
  -h,--help                Show this help text

The input source file can either be a source file or a raw binary RPN image (using .raw file extension).

The default behavior is to convert XEQ to a global alpha label, to be its corresponding ROM based XROM instruction. The XROM variant will always call the same function and uses less memory, see Global alpha labels.

Normally you do not need to use any particular option, unless you want to preserve an XEQ to a global alpha label which may be used as a mechanism to call a user written function stored in RAM. A more flexible way to make such call, is to store the global alpha label name in a register and call it indirectly.

In addition to creating relocatable files, the RPN compiler can convert .raw files to readable .rpn source files. Use the --rpn-output for this, see Converting .raw files for more details.

6.1. RPN language

The language used is similar to how it appears in the HP-41 with some minor differences due to special characters.

The RPN compiler expects the input file to be encoded in UTF-8, which means that ordinary ASCII characters are encoded as (7-bit) ASCII characters, while non-ASCII characters are encoded in sequences of multiple bytes (with the highest bit set, making them different from all ASCII characters). Thus, it is possible to type an instruction such as ΣREG in the way it appears in the HP-41, which is impossible to do in ASCII.

In order to help typing programs, some synonyms are also accepted:

Table 6.1 RPN synonyms

RPN

Synonyms

CLΣ

CLSIGMA

ΣREG

SIGMAREG

Σ+

SIGMA+

Σ-

SIGMA-

÷

/

X≤Y?

X<=Y?

X≤0?

X<=0?

X≠Y?

X#Y?, X!=Y?

X≠0?

X#0?, X!=0?

ENTER^

ENTER

6.2. Preprocessor

The RPN compiler uses a full featured C preprocessor to handle the input source file. The preprocessor provides the normal features you will find in a C preprocessor, the ability to include header files, macro expansions, conditional compilation and use of C style comments.

Wikipedia is a good place to look for an introduction with many examples on how to use the C preprocessor.

When used in the RPN compiler, the following macros are predefined:

Table 6.2 Predefined processor symbols

Preprocessor symbol

Description

__CALYPSI_NUT__

An integer that is 1 when the Nut target (including NEWT variant) is used.

__CALYPSI_RPN_COMPILER__

An integer that is 1 when the RPN compiler is used.

__CALYPSI_MODULE_USE__

An integer that is 1 when generating code for module use.

__CALYPSI_RAM_USE__

An integer that is 1 when generating code for RAM use. (--raw-output)

Comments

Comments start with a semi-colon and remains active for the rest of the line. Using C style comments are also possible thanks to the preprocessor.

Line numbers

HP-41 RPN programs have line numbers as listed. The RPN compiler will accept programs as input either with or without line numbers. Whether the program have line numbers is sensed automatically by looking at what appears to be the first instruction in the program. If that is a number, it is assumed that there will be a line number before each instruction. 1

Line numbers are just read and ignored and there is no requirement that they make any sense, such as being in a strict increasing order.

Indentation

Spaces can be used to indent code, which can be useful to increase readability. Using empty lines and comments blocks may also be useful to make the program easier to read.

Alpha strings

As the input file is assumed to be in UTF-8, it allows you to use certain special characters not available in ASCII and type them as they look. Refer to the Unicode table in HP-41 character set.

Append alpha

If the append character appears first in an alpha literal, it means that the string literal will be appended to the alpha register rather than replacing it.

The append character is 127 which correspond to the delete character in ASCII. While it is possible to type it using the Unicode character, or a hexadecimal or octal escape sequence (see below), the easiest way is to precede the string literal with the greater than sign:

>"FOO"   ; Append FOO

Control characters

Control or individual special characters inside an otherwise normal string literal, can be inserted using escape sequences in the same way as in the C language:

"ABC\r"    ; followed by CR
"ABC\x0d"  ; also followed by CR (hex 0D)
"FOO\\BAR" ; \ itself must be escaped

Synthetic alpha strings

If you want to enter an string literal where the actual numeric character codes are more important than readable characters, the alternative syntax with a bracketed list of character codes can be used:

[128,255,0,3]   ; 4 characters in decimal form

Numeric constants

Numeric constants are written just as they appears in an HP-41 program. You need to separate the exponent field from the number with a space. Synthetic exponent constants are also allowed:

-1.303 E-13
E
E3
-E

NULL separation of numeric constants

If the source program contains two numeric constants immediately after each other, an invisible NULL will be inserted between them. This is done automatically. Such NULL instruction will not get an RPN line number, as it is invisible when examining the program in the HP-41. The NULL instruction will however be visible in the list file, without any line number will be assigned to it.

6.3. A basic example

A simple RPN program to calculate the area of a circle can be implemented as follows:

;; Calculate the area of a circle given radius in X

LBL "AREA"
X^2
PI
*
END

Running the RPN compiler on it produces a list file with the following contents:

###############################################################################
#                                                                             #
# Calypsi RPN compiler for Hewlett-Packard Nut                   version 5.16 #
#                                                       14/Apr/2026  16:42:38 #
# Command line: -l area.rpn                                                   #
#                                                                             #
###############################################################################

0000                    .section FAT
0000 ......             .fatrpn AREA
0000                    .section RPN
0000 002270             ;; COPY header
0002 1c6000               01 LBL "AREA"
0004 0f5000
0006 041052
0008 045041
000a 151                  02 X^2
000b 172                  03 PI
000c 142                  04 *
000d 1c8001               05 END
000f 22f

##########################
#                        #
# Memory sizes (decimal) #
#                        #
##########################

Executable  (Text): 18 words

As can be seen from the list file, there are two sections generated. The first one (FAT) is meant to go into the function address table. We are after all going to produce a module and the global alpha label needs to be included there in order to be accessible. The actual word values to be put into the image cannot be determined at this point, as we do not know where the program is going to be located in the module.

The second section (RPN) is the actual program. It starts with two words that describe some properties of the program meant to be used by the COPY instruction, which ensures that the program can be properly copied to RAM memory. The rest are the actual RPN instructions encoded for ROM use.

Also note that RPN line numbers are generated before each RPN instruction. The leftmost column of hex numbers are the offset for the instruction within its section.

6.4. Branches and labels

Relative branches are automatically compiled for ROM use. The HP-41 can make use of short and long branches and the smallest one that can reach the destination is used. 2

6.5. Global alpha labels

Global alpha labels are automatically found and a suitable information is generated for populating the FAT for each label. By default you will get a small FAT section fragment with this program’s contribution to the final FAT. You may want to control this by hand, see FAT order.

A call to a global alpha label using XEQ is assumed to be within the current module. It is converted to a 2-word XROM instruction, where the generated words depends on the final location in the FAT. The actual values of these words will be determined at link time.

In some cases you may actually want to call a global alpha label and avoid the automatic translation to XROM. You can do this using the --xeq-from-rom command line switch:

LBL "FOO"
XEQ "AREA"
XEQ "FN"
END

To prevent FN from being converted to an XROM call, use the following command line:

$ rpncomp foo.rpn --xeq-from-rom=FN

In the resulting list file, FN has been preserved as an XEQ to a global alpha label. This will cause the operating system to search for it starting in catalog 1.

###############################################################################
#                                                                             #
# Calypsi RPN compiler for Hewlett-Packard Nut                   version 5.16 #
#                                                       14/Apr/2026  16:42:38 #
# Command line: -l call.rpn --xeq-from-rom=FN                                 #
#                                                                             #
###############################################################################

0000                    .section FAT
0000 ......             .fatrpn FOO
0000                    .section RPN
0000 003220             ;; COPY header
0002 1c2001               01 LBL "FOO"
0004 0f4000
0006 04604f
0008 04f
0009 ......               02 XROM "AREA"
000b 11e0f2               03 XEQ "FN"
000d 04604e
000f 1cc001               04 END
0011 22f

##########################
#                        #
# Memory sizes (decimal) #
#                        #
##########################

Executable  (Text): 20 words

If you want to prevent multiple routines from being converted to XROM calls, either use the --xeq-from-rom option multiple times, or specify a double quoted space separated alpha label list:

$ rpncomp foo.rpn --xeq-from-rom="FN AREA"

Note

The default behavior when calling a global alpha label with XEQ is to convert it into an XROM call instruction for the module being built. If that global alpha label is not defined in the same RPN program, the RPN compiler will silently create an external dependency on it. The linker will later take care of fixing the XROM call, provided that the global alpha label exists in another RPN program linked together in the same module.

6.6. Using other plug-in modules

An RPN program can use instructions and functions defined in other plug-in modules. Such modules are not known to the RPN compiler from start, but they can be made available using an .import directive in the source file.

In this example the optional HP-41CX instructions and the Math Pac module is imported. The CLRGX instruction is part of the HP-41CX and the RPN program ACOSH is defined in Math Pac:

.import 41cx, math

01 LBL "TEST" 
02 3.065
03 CLRGX
04 XROM "ACOSH"
05 END

Note that instructions are just typed as any instruction, while the function ACOSH in Math Pac is an RPN program so it needs to be preceded by XROM, just as it appears when the HP-41 displays the program steps. Compiling this program produces the following list file output:

###############################################################################
#                                                                             #
# Calypsi RPN compiler for Hewlett-Packard Nut                   version 5.16 #
#                                                       14/Apr/2026  16:42:38 #
# Command line: -l import.rpn                                                 #
#                                                                             #
###############################################################################

0000                    .section FAT
0000 ......             .fatrpn TEST
0000                    .section RPN
0000 003260             ;; COPY header
0002 1c8000               01 LBL "TEST"
0004 0f5000
0006 054045
0008 053054
000a 11301a               02 3.065
000c 010016
000e 015
000f 1a6072               03 CLRGX
0011 1a0066               04 XROM "ACOSH"
0013 1c6002               05 END
0015 22f

##########################
#                        #
# Memory sizes (decimal) #
#                        #
##########################

Executable  (Text): 24 words

As the XROM codes for these optional functions are known at compile time (the modules already exists, it is nothing we are generating now), the actual words are resolved at compile time.

The imported modules have to be supplied as separate module export files. How the imports can be generated is described in the Module tool chapter.

The installation contains a set of already generated module export files in the module-export directory. If the desired module is available there, you can just use it as the RPN provides contains this directory in its predefined search path.

The search path also includes the current directory. If the module export file is located elsewhere, you need to use the -I command line option to add that directory to the search path.

6.7. Invoking local MCODE instructions

A special case is if you are building a mixed RPN and MCODE module. There are a couple of ways of doing this. In general it boils down to whether:

  1. you decide up front on the XROM numbers for your MCODE instructions, or

  2. you want to let the tool help you in a similar way to local RPN functions.

If you want to have fixed numbers, you can define a module exports file and let the RPN functions import it. Then you just need to ensure that you link the code in such order that the MCODE instructions end up at the specified location in the FAT.

The alternative is to let the tools figure it out using a technique that piggybacks on the same mechanism that allows automatic calls of RPN functions in the same module.

To do this you need to tell the RPN compiler that there are local MCODE instructions using the .local directive. 3 It looks syntactically similar to the .import directive, but instead of reading a module exports file, it specifies individual MCODE instruction names. In the Poker game that is part of the supplied example games module (see RPN games module), there are three MCODE support instructions called CARD, MV and TRI. The start of the source file may then be as follows:

.import 41cx

#ifdef __CALYPSI_MODULE_USE__
.local CARD, MV, TRI
#else
.import exgames
#endif

01  LBL "POKER+"
02  10
   ...
54  CARD

Note

Using .local is only needed when importing MCODE instruction names. It is not needed for RPN programs. The reason is that RPN programs are wth an XROM prefix, so the compiler have no problem understanding what is going on.

This makes the MCODE instructions available to the RPN program. The actual XROM code used is unknown at compile time and the mechanism works as follows. It is expected that for each such MCODE instruction, there is a special named public label at each FAT entry. In the MCODE source file you will need to do something like: 4

              .section FAT
`FAT entry: CARD`:
              .fat  entry_CARD

`FAT entry: MV`:
              .fat  entry_MV

`FAT entry: TRI`:
              .fat  entry_TRI

              .public `FAT entry: CARD`, `FAT entry: MV`, `FAT entry: TRI`

              .section CODE
              .name "TRI"
entry_TRI:    c=regn c
              rcr   3

The FAT labels are expected to be named like `FAT entry: XX` where XX is the name of the MCODE instruction as given to the .local directive. Since this label contains non-alphanumeric characters, it needs to be surrounded by back quotes.

If you expect your functions and instructions to be used from other programs or modules, you may feel that you need control the order in which the entries go into the FAT. This is possible by achieve by using different section names and by the order in which the object files are specified to the linker on the command line.

6.8. Converting .raw files

Since .raw files are just raw binary data, they are not human readable. Sometimes it makes sense to convert such programs into readable text, for inspection or editing. You can perform such conversion using the --rpn-output option. In addition to this option, you need to specify a .raw file. The output file is written to the current directory, using the same base name as the .raw file, but with a .rpn file extension instead.

Calls to external ROMs currently shows up as XROM instructions with numbers.

If you have many .raw files you want to convert, here is a way using xargs which is available if you are using a POSIX.2 compliant system:

$ ls path-to-raw-files/*.raw | xargs -n1 -J % rpncomp --rpn-output %

Footnotes

1

This means that a program which does not have line numbers but where the first instruction is a numeric constant, cannot be handled properly. In reality, this is a minor limitation as most programs start with a global alpha label.

2

Short jump reachability differs slightly in ROM and RAM on the HP-41. In a ROM the distance covered by a short branch is slightly longer compared to RAM. This means that while a short branch may be compiled properly in ROM, it may not be compilable when copied to RAM. This will only affect the execution speed of the program, it will otherwise work in the same way.

3

The .local directive only works when building a module, as there is no FAT in RAM memory. In order to make the RPN program work in both RAM and a module, use conditional preprocessor #ifdef on the __CALYPSI_MODULE_USE__ symbol around it and provide a module exports file for RAM use, see the example.

4

This manual work is not needed when using RPN functions, as the compiler will create the FAT entries and public labels automatically.

6.9. Merged XROM instructions

Some modules allow XROM instructions to have postfix operands. Such combined instructions are not supported by the HP-41 operating system, but is possible using some special techniques. There are a couple of variants available, and they rely on that the XROM instruction consumes the following step (which is an ordinary stand alone RPN instruction) and use it as an argument of some kind.

Current support for this is limited and only supports the style used by the Ladybug module.

The .postfix directive tells the RPN compiler that you have an XROM with some kind of postfix operand. It takes a list of instructions it applies to, followed by the operand style they use inside parentheses:

.import ladybug

.postfix DSZI, DECI, INCI, LDI, STI  (semi-merged-postfix 00)
.postfix CLRI, CB, SB, VIEWI, B?     (semi-merged-postfix 00)
.postfix TST                         (semi-merged-postfix 00)
.postfix SL, SR, ASR, RL, RR         (semi-merged-postfix 01)
.postfix RLC, RRC                    (semi-merged-postfix 01)
.postfix MASKL, MASKR                (semi-merged-postfix  8)
.postfix SEX, WSIZE                  (semi-merged-postfix 16)
.postfix ALDI, BITSUM                (semi-merged-postfix  X)
.postfix CMP                         (semi-merged-postfix  Y)
.postfix #LIT                        (semi-merged-integer-literal)
.postfix =I, ≠I, <I, <=I, <>I        (semi-merged-dual)

#define integer #LIT

LBL "LADY"
WSIZE 16
integer 0xffe
<>I 01 M
STI IND 02
MASKL 4
LBL 00
VIEWI X
PSE
DSZI X
GTO 00
END

As can be seen, you still need to import the module to being in the available instructions. The .postfix directive is used to dress them up as instructions that actually take operands.

Note

The integer literal instruction (#LIT) starts with a hash symbol which will cause an error from the C preprocessor if used first on a line, as the preprocessor will try to interpret it as a preprocessor directive. To work around it, the #define defines a synonym integer which expands to the #LIT instruction.

Following the postfix style, the default argument can optionally be specified. If it is specified, the compiler can avoid generating an explicit postfix instruction when the default will perform the same operation. WSIZE uses an argument that is the same as the default in this example program.

In the example VIEWI is a secondary function and gets an extra byte that represents its function code due to this.

Dual argument functions as <>I are entered with the function name first followed by its two operands. This is also how they are entered from the keyboard on the HP-41.

The following list file shows the result:

###############################################################################
#                                                                             #
# Calypsi RPN compiler for Hewlett-Packard Nut                   version 5.16 #
#                                                       14/Apr/2026  16:42:38 #
# Command line: -l ladybug-postfix.rpn                                        #
#                                                                             #
###############################################################################

0000                    .section FAT
0000 ......             .fatrpn LADY
0000                    .section RPN
0000 007230             ;; COPY header
0002 1c0001               01 LBL "LADY"
0004 0f5000
0006 04c041
0008 044059
000a 1a4008               02 WSIZE
000c 1a4001               03 #LIT 0xffe
000e 1f200f               04 [15,254]
0010 0fe
0011 1a403a               05 <>I 01 M
0013 1f3004               06 [4,1,117]
0015 001075
0017 1a402c               07 STI IND 02
0019 1f1082               08 [130]
001b 1a4029               09 MASKL 04
001d 1f1004               10 [4]
001f 101                  11 LBL 00
0020 1a403a               12 VIEWI X
0022 1f2005               13 [5,115]
0024 073
0025 189                  14 PSE
0026 1a4031               15 DSZI X
0028 1f1073               16 "s"
002a 1b100d               17 GTO 00
002c 1c0006               18 END
002e 22f

##########################
#                        #
# Memory sizes (decimal) #
#                        #
##########################

Executable  (Text): 49 words

In the generated listing, such XROM instructions are generated as two separate instructions. The postfix byte is held in alpha string literals following the actual instruction. To make it easier to read and save you from having to look it up from some table, instructions are shown together with their operands. However, close studying of the generated opcodes and line numbers reveals that they are actually two instructions from a program list point of view.