Enter RPN

Files in hp15c/EML/ of trunk
Login

Files in hp15c/EML/ of trunk

Files in directory hp15c/EML from the latest check-in of branch trunk


EML/Suite for the HP-15C

This is EML/Suite ported to the HP-15C, SwissMicros DM15, and assorted simulators and emulators.

Rather than key in all this code on-device, I wrote the source code in the one true programmer’s text editor,1 then used rejig to produce the program file:

$ rejig Suite.hp15c -o Suite.15c

Or, thanks to the provided Makefile, just say make.

The result may be loaded into any of the machines linked above. I got much use of Torsten Manz’ simulator in producing this port, both directly on my development machines and as a USB I/O bridge to my DML15L and HP-15C Collector’s Edition, facilitating the benchmarks below.

Usage

This suite’s subroutines had to be renamed from those in the reference implementation to fit within the label scheme used on the 15C family. Rather than give rejig free rein in mapping names, I chose mnemonics:

Label Fn Stk Description, Mnemonic
A `y+x` 3 Add
B `y×x` 2 multiply By
C `π` 4 𝜋 = Circle ratio
D `y-x` 1 Debit = subtract
E `eml(x,y)` 0 Exp-Minus-Log
0 `0` 2 0 = zero
1 `ln(x)` 1 natural log; 1 ≈ “l”
2 `2` 3 2
3 `exp(x)` 1 3xponential, `e^x`
4 `x^2` 3 square = 4-sided
5 `sqrt(x^2+y^2)` 4 hypot; 5/4/3 triangle
6 expit(x) 3 logistic sigmoid; 6 ≈ sig
7 Im 1 7th heaven = imaginary
8 BPA 4 ballpark accuracy; 8-ball
9 none 4 stack usage test; 9=last
.0 `-x` 3 negate
.1 `-1` 3 negative 1
.2 `x÷2` 4 half = divide by 2
.3 `pow(x,y)` 2 power = 3xponent
.4 `sqrt(x)` 3 root = inverse of squaring
.5 `-x` 0 5 ≈ faux -x helper for D
.6 `avg` 4 avera6e
.7 `y÷x` 2 divide; 7solidus
.8 `log_x(y)` 2 ar8itrary log
.9 `1÷x` 2 9vert (reciprocal)

Before running any of these, be sure your 15C is in complex mode by calling the 🟧 I function, above the TAN key.

Resources

Operands are consumed and replaced with results in the same manner as the builtin equivalents. Prior elements on the stack may be overwritten by intermediate results, but no “litter” is left behind.

The reference implementation uses the R47’s LocR feature to allocate local scratchpad registers at need. Lacking anything like that in the 15C series, this port uses global registers instead:

Regs Usage
R0-R1 general scratchpad
R3-R4 hypotenuse (LBL 5)
R5-R6 ballpark accuracy test (LBL 8)

We need this many registers to avoid overlapping uses in nested calls even though it is never using more than two temporaries per subroutine.

Limitations

Labels

This port is incomplete primarily for lack of labels. I’ve used all 25 available, and even then I had to drop inessentials and inline all the transcendentals into a monolithic ballpark accuracy test. The trigonometric and hyperbolic functions are present within subroutine 8, but without LBL targets and separate RTN for each, they aren’t readily called.

The scant mnemonics allowed by A-E and the predominance of numbered labels resulted in the jokey scheme you see above. I used English puns plus the rule that . labels are for inversions, divisions, and negations by riffing on .0 as the `-x` operation. Thus LBL 4 is for the squaring operation since squares are 4-sided, making the inverse `sqrt(x)` operation LBL .4.

Error 0

Since the HP-15C refuses to calculate `ln(0)=-oo`, we include a manual workaround for its lack. Unlike the HP-12C and HP-32S family ports, though, the 15C has full-featured complex number support, allowing it to calculate `ln(x<0)`, giving wide-ranging argument support to the included subroutines.

If you continue to get Error 0 while playing with this port, it is because you ignored the instruction above.

Subroutine Call Depth

Once you get beyond the division operation (LBL .7) the call depths exceed the 8-level call stack in the HP-15C.

Or they would, had I not perpetrated manual tail call optimization by replacing GSB+RTN pairs with GTO. This hack causes nearly all entrypoints to chain as one, with a final GTO E acting as the implicit RTN. The number of places I was able to do this makes perfect sense in retrospect since the very purpose of “1” in the EML scheme is to allow `eml(x,y)` to arrive at useful end-points.

If you wish to use rejig --fmt on this — as via the included make fmt target — you will need to put a RTN at the end of each subroutine lacking one. This is purely because rejig does not yet recognize TCO, fooling it into indenting each subroutine under the one prior as sub-sub-subroutine.

Memory Use

Classic HP-15C users will need to say 6 f DIM f (i) before keying in this code beyond LBL 7, else they will get the dreaded Error 4 before reaching the end. The subsequent subroutine is the ballpark accuracy test, LBL 8, which had to be inlined for lack of labels. The incestuous nature of EML resulted in a shocking2 amount of code redundancy, blowing the stock program memory limit.

(Cramming it into even that limit required a fair bit of optimization!)

Mind, this occurs within the other EML versions as well; it’s merely obscured by all those XEQ calls. (GSB in HP-15C parlance.)

You might think we could avoid a portion of this undesirable redundancy by saving intermediate results to registers where later reuse is possible, but keep in mind that on the HP-15C, program space trades off against the chosen register allocation. Each additional register you allocate needs to save a net 7 lines of code before it reaches net-zero; more if you expect the tradeoff to reduce net memory usage.

The cost of running redundant code instead of RCL-ing a previously-calculated intermediate is a slower benchmark result. You might therefore choose to burn what remains of your chosen device’s remaining program space to buy speed. On the original hardware, it is presently possible to raise the DIM limit to 8 before you start getting Error 10, the calculator’s way of saying, “My designers were not foolish enough to make me unceremoniously clip off the end of your program to meet this new limit.” On 15C-alikes blessed with higher limits, you should be able to allocate more than 8 total registers while this port is keyed in.

Beware, this won’t buy much; you will definitely spend more memory space in register use than you save in program space. This is why I leave the option to you.

Personally, I was pleased merely to get a reasonably featureful port of EML/Suite to fit within these ancient limits, period; my first few attempts did not. The biggest wins were getting rid of those cometic RTN calls mentioned above, plus switching to stack-dirtying R↓ ops instead of the nicer CL𝑥-then-add pairs used elsewhere in the suite for dropping intermediate results. Out here at the leaf of the call graph, we no longer have to clean up after ourselves.

Setting an explicit register allocation is unnecessary on the HP-15C Collector’s Edition due to its more generous stock3 memory limit.

Special accommodations are even less necessary on the SwissMicros DM15 hardware. Its stock configuration maxes out the programming model at the 999-step limit afforded by the HP-15C’s 3-digit addressing scheme.

The software versions vary in how they handle this:

Speed

When using an HP-15C simulator on a fast PC, it’s difficult to notice one of the several impracticalities of this software-coded version of EML: it’s terribly inefficient. On-device, things can begin to take human-scale time, though it remains easy to be misled; calculating `e` via 1 GSB 3 runs in an eyeblink even on that 1982 powerhouse, the original HP-15C, but realize that this is because it ends up calling into the native implementation of `e^x` after a few steps. Matters change when you ask it for something that should be as easy as 𝜋, via GSB C:

Device GSB Time Speedup
HP-15C 0:03:41 1.0
DM15L, 12 MHz 0:00:46 4.8
DM15L, 48 MHz 0:00:11 20.1
15C CE 0:00:01.7 130
Manz sim ~1 ms ludicrous

Taking a few seconds to compute 𝜋 even on the fastest available hardware is bad enough, but let us now take the ballpark accuracy test via GSB 8:

Device GSB Time Speedup
HP-15C 1:08:08 1.0
DM15L, 12 MHz 0:14:05 4.8
DM15L, 48 MHz 0:03:15 21.0
15C CE 0:00:29 141.0
Manz sim a second plaid

You may now be asking yourself, “Did that lunatic actually let that slowpoke HP-15C cook for over an hour whilst hovering with a stopwatch, never breaking attention long enough to meaningfully skew the result? Truly now?” The answer is no, because several iterations into this project, I finally got smart: I clamped my smartphone into a tabletop tripod rig, fired up the video camera app, started the test, and then went off and did something productive.

(Vacuuming the cats’ winter coat sheddings, if you must know.)

Once I noticed that the slowest machine in each wave hit its idle timeout and shut itself off, I stopped the video, trimmed off the test setup leader, and then scrubbed forward to find the first frame where each machine gave its result.

Yes, this means I filled the flash RAM of an overpriced smartphone with an hour of an HP-15C blinking running purely to produce the effect of an automatic stopwatch. (Not to mention burning bandwidth syncing it, because ooooobviously one wants such a treasure safely archived in the cloud forever and ever!) If there’s a special hell for the lazy and inefficient, I’m well on my way now! 😜

License

These programs are © 2026 by Warren Young and offered under the terms of the MIT License.


  1. ^ In this specific case Cursor with the Neovim plugin, but vi generically, being the proper baseline for writing software. I have spoken.
  2. ^ The worst is near the end, where we have two back-to-back inlined copies of EMLACos, which follows not far after a third!
  3. ^ As opposed to the double-memory mode via the hidden 15.2 option on the Easter egg menu.
  4. ^ The distinction being, an emulator uses the original firmware running atop a software emulation of the original CPU, whereas a mere simuator uses a pure software reimplementation of the algorithms, running more or less directly atop the host CPU. (“Less” in the case of JRPN owing to the Flutter and JavaScript layers it interposes.)