System RPL is the unchecked, internal layer User RPL sits on — the language the ROM itself is written in. Page 1 is a working introduction: the toolchain on the calculator, source format, bints, argument dispatch, locals, tests, loops and errors. The index lists every supported entry from Programming in System RPL (2nd ed.) by the book’s own chapters, each with its 49/50 ROM address and stack diagram. Hover an entry for the full description. The safe, documented layer lives on the HP 50g RPL sheet.
A bint is not a number the way a % real is a number — it is a whole object, ten nibbles of memory. The first five are a prologue: the address of the ROM routine that knows how to handle this type. The last five are the value — twenty bits, 0 to 1,048,575. That is the entire type. Twenty bits is exactly one Saturn address, which is why every count, index, keycode and pixel coordinate on this sheet is a #: the machine has no cheaper number.
Writing # 13 in your source compiles that whole ten-nibble object into the program. Writing BINT19 compiles one five-nibble address — 2.5 bytes — pointing at a bint that already exists in ROM. The ROM keeps 132 of them, BINT0 at 33107 and each next one exactly ten nibbles on, so FIFTYSIX (BINT56, the smiley’s starting x) sits at 33337. That is the whole reason ROM listings are full of named constants instead of literals.
Shown above as you would write it. How those five nibbles actually lie in memory is the next panel. Not to be confused with the user-level # binaries you type on the calculator — those are hex strings (chapter 7), a different object.
A secondary is not compiled code. It is a list of addresses: :: is DOCOL, everything up to ; (SEMI) is one five-nibble pointer per word, and the interpreter’s runstream pointer walks them left to right. Almost every odd corner of the language falls out of that one fact.
A word costs 2.5 bytes because it is one address. ' ob tells the interpreter to push the next address instead of jumping to it — that is all quoting is. And when ITE or #<ITE is said to “take the next two objects”, it literally reads two cells out of the runstream and steps over them, which is why a bare #10+ is a complete branch: one word is one cell.
Multi-nibble fields are stored least significant nibble first. Nothing in the source hints at it; you either know it or the data comes out as noise.
So the smiley grob’s height field, which appears in the listing as 31000, is read backwards as 00013 — 0x13, nineteen. Same five nibbles, and the difference between a 19-pixel sprite and a 200,704-pixel one. This is why the walkthrough at the foot of this page can say the sprite is 19 × 19 when the listing appears to say something else entirely.
A grob row is padded up to a whole byte, and inside each nibble the low bit is the leftmost pixel. So 19 pixels occupy three bytes with 5 bits of slack, and the bit you would write first is the one you read last.
That is row 6 of the smiley — its eyes. Read the nibbles left to right as printed; inside each one, bit 0 is the leftmost pixel. Equivalently: each printed pair of nibbles is one byte stored low-nibble-first. Get that backwards and the face comes out mirrored in four-pixel blocks, which is exactly what a first attempt looks like.
System RPL is the language the calculator’s own ROM is written in — the layer under User RPL. Same threaded interpreter, same stack, but no seatbelts: entries are called directly, nothing checks your arguments, numeric arguments are bints (20-bit system binaries) rather than reals, and a mistake corrupts memory instead of raising a polite error. In exchange a typical program runs 3–10× faster than its User RPL twin (the book’s circle-area example: 1.9 ms vs 15.6 ms).
Programs are built from supported entries — ROM addresses HP promised would stay put, each with a name in the community entry table. This sheet indexes 3,260 of them with their HP 49/50 addresses and stack diagrams. Names beginning ˆ are flash pointers (FPTR); six-digit addresses are rompointers/flash — MASD resolves all of them by name when extable is installed. Addresses are for the 49G/49g+/50g line; the HP 48 uses the same names at different addresses.
Back up first, always: a wrong entry ends in “Try To Recover Memory?”. Develop in the emulator, run on hardware when it works.
Everything in ( ) is a comment; * in column 1 comments a line; System RPL is case-sensitive. Literals: # 3A a bint (hex), % 1.5 a real, $ "text" a string, ' ob quotes the next object to the stack. Assembly goes between CODE … ENDCODE. The example above is the book’s: 25 bytes, eight times faster than « SQ π * →NUM ».
Diagram shorthand used throughout the index: ob any object · # bint · % real · %% extended (15-digit) real · C%/%%C complex · $ string · CHR character · HXS hex string · Z zint · id/lam names · seco secondary · {} list · arry array · grob graphic · T/F/flag the TRUE/FALSE objects · meta = obs + count.
Bints are the workhorse: every count, index and coordinate. BINT0…BINT255 and friends push constants from ROM at 2.5 bytes each; #+ #- #* #/ do arithmetic; R~SB/SB~B (library 256) convert on the command line. Extended reals (%%) carry 15 digits for accurate intermediates — %>%% and back. Meta objects are n objects plus their count on the stack — the CAS lives on them.
A command begins by checking its arguments once — then runs unchecked. CK0NOLASTWD…CK5NOLASTWD demand n arguments (CKn alone also saves the command name for error reports); CK&DISPATCH0/CK&DISPATCH1 branch on types (the 1-variant retries after stripping tags and turning zints to reals); CKn&Dispatch combines both.
Type digits, level 5→1 (from ch. 29): 0 any · 1 real · 2 cmp · 3 $ · 4 arry · 5 list · 6 id · 7 lam · 8 seco · 9 symb · B hxs · C grob · D tagged · E unit · two-digit codes 1F bint · 3F %% · 6F chr · FF zint … MASD also accepts the words real, cmp, str, lst, idnt, sym, grob for single arguments.
Named LAMs read clearest; numbered GETLAMs are what the ROM itself uses in hot paths. Nothing checks that an environment exists — an unmatched ABND is a crash.
Errors thrown between BIND and ABND leak the environment unless the trap abandons it; the ROM idiom is ERRSET … ERRTRAP :: ABND ERRJMP ;.
This sheet is the reference half. The tutorial half is the book it was generated from: Programming in System RPL, 2nd ed. (Kalinowski & Dominik, 640 pp., free on hpcalc.org) — read chapters 1–3, 17–22 and 29 and you can write real programs. HP’s original RPLMAN (48-era) and the built-in disassembler (DISASM on any ROM word) fill in the rest. The User RPL layer, the menus and every user command are on the HP 50g RPL sheet; DB48X reimplements this whole world in modern C++.
Entries are named as extable names them; ˆ marks flash pointers. Addresses are 49G-series; they are what SYSEVAL wants (#3188h SYSEVAL is DUP).
Section 32.8 of Programming in System RPL is the book’s one complete application, and the place most people first meet a parameterized outer loop. It puts a small smiling face on the screen and lets you push it around with the arrow keys: one pixel a press, ten if you hold left-shift first, and the same four moves again on the menu keys. ON or the Quit menu key ends it.
Nothing in it is advanced. What makes it hard to read cold is that four System RPL habits arrive at once — objects that are pushed rather than run, case words that return from the whole program, tests fused into branches, and arithmetic done in bints. Taken one block at a time it is about ninety lines of genuine code and a hundred of repetition. Below, every block with what it actually leaves on the stack.
Line numbers refer to the listing on pp. 218–223. The four arrow handlers and the four menu keys are near-identical, so one of each is shown.
GROB literal on line 21. It starts at (56, 18): dead centre of the 131 × 56 text display.:: DEFINE kpNoShift BINT1 DEFINE kpLeftShift BINT2 DEFINE kcUpArrow BINT10 DEFINE kcLeftArrow BINT14 DEFINE kcDownArrow BINT15 DEFINE kcRightArrow BINT16 DEFINE kcLeftShift BINT37 DEFINE kcOn BINT47 CK0NOLASTWD (no arguments wanted) RECLAIMDISP (clear and resize display) ClrDA1IsStat (temporarily disable clock)
DEFINE is a MASD compile-time macro — plain text substitution, free at run time. Two kinds of name are being defined: kp… are key planes (BINT1 unshifted, BINT2 left-shifted) and kc… are key codes. Both arrive as bints, which is why the constants are BINTn and not reals.
CK0NOLASTWD is the argument check: “I take nothing.” The NOLASTWD half means it will not save the command name for an error message — cheaper, and there is no name worth saving here.
RECLAIMDISP claims the text display, clears it, and resizes it to the default 131 × 56 pixels. Remember that number; every odd-looking constant later in the program is derived from it. ClrDA1IsStat suspends the ticking clock, which would otherwise redraw itself over the top line.
GROB 7C 310003100008F000060300810C00400010400010200020…
FIFTYSIX (initial x coordinate)
EIGHTEEN (initial y coordinate)
FALSE (initial exit condition)
{
LAM MrSmile
LAM x
LAM y
LAM exit?
} BIND (binds local variables)The GROB literal is one long line of hex — the book warns it must not be wrapped. Read as an object body it is: 7C, then two five-nibble little-endian fields 31000 31000 (each reverses to 00013 = 19) for height and width, then 114 nibbles of pixels — 19 rows of three bytes, since grob rows pad up to a whole byte and inside each nibble the low bit is the leftmost pixel — see Anatomy. The arithmetic closes: 0x7C = 124 = 5 + 5 + 114.
FIFTYSIX and EIGHTEEN are named bint constants — the ROM keeps dozens of them, 2.5 bytes each, cheaper than assembling the number. They are (131 − 19) ÷ 2 and (56 − 19) ÷ 2: the sprite starts centred.
BIND takes the four objects off the stack into named lams. The order catches everyone: binding works from the deepest stack level towards level 1, so the first name in the list gets the deepest object. MrSmile ← the grob, x ← 56, y ← 18, exit? ← FALSE. From here the four names are the program’s entire state.
' ::
CLEARVDISP (clear display)
LAM MrSmile (recall smiling face grob)
HARDBUFF (recall current display)
LAM x LAM y (smile coordinates)
GROB! (REPL)
DispMenu.1 (display menu)
;The leading ' is the whole trick: it quotes the secondary, pushing it on the stack as an object instead of running it. This one becomes the loop’s AppDisplay parameter, and the loop evaluates it before every keypress.
That is why no key handler ever touches the screen: the keys only move numbers in x and y, and this object redraws from them. Clear, blit, done — no dirty-rectangle bookkeeping, and no way for the picture and the variables to disagree.
HARDBUFF ( → dispgrob ) hands you the live display grob, so writing to it writes to the screen. GROB! ( grob1 grob2 #x #y → ) stores grob1 into grob2 at (x, y) and returns nothing — a “bang” word that edits in place. If you go looking for a result grob to store back, there isn’t one.
' ::
kpNoShift #=casedrop ::
DUP#<7 casedrpfls (let the softkeys through)
kcUpArrow ?CaseKeyDef :: … ;
kcDownArrow ?CaseKeyDef :: … ;
kcLeftArrow ?CaseKeyDef :: … ;
kcRightArrow ?CaseKeyDef :: … ;
kcOn ?CaseKeyDef :: TRUE ' LAM exit? STO ;
kcLeftShift #=casedrpfls
DROP 'DoBadKeyT
;
kpLeftShift #=casedrop :: … ;
2DROP 'DoBadKeyT
;The AppKeys contract is ( #KeyCode #Plane → KeyDef TRUE ) if you handle the key, or ( #KeyCode #Plane → FALSE ) if you don’t. So this object is read outside-in: first pick the plane, then the key.
Three case words do all the work, and each one returns from the entire secondary when it fires — not just from its clause:
#=casedrop( #m #n → … ) equal: drop both, run the following object, exit. Not equal: drop only #n and carry on. Inside the clause only the keycode is left.?CaseKeyDef( # #' → ob T ) the specialist: equal → drop both, quote the next object, return it with TRUE and exit. It is exactly #=casedrop :: ' <keydef> TRUE ; in one entry.casedrpfls( ob T → F ) if the flag is TRUE, drop the object and return FALSE.So DUP#<7 casedrpfls reads: copy the keycode, is it under 7 — a softkey? — then bail out with FALSE. FALSE means “not mine.” The loop then runs the standard definition for that key, which is how the menu keys and the shift key keep working at all. 'DoBadKeyT ( → DoBadKey T ) is the opposite: “mine, and the answer is a beep.”
kcUpArrow ?CaseKeyDef
::
LAM y DUP
BINT1 #<ITE
:: DROP ERRBEEP ;
:: #1- ' LAM y STO ;
;LAM yyDUPy yBINT1y y #1#<ITEy — the test ate the copy and the 1, then chose one of the two following objectsDROP ERRBEEP— empty; beep#1- ' LAM y STO— empty; y−1 storedThe DUP is not decoration. #< consumes both its arguments, so without the copy the branch would have nothing to work on. This copy-test-consume shape is everywhere in ROM code.
#<ITE is a fused compare-and-branch, and like ITE it takes the next two objects out of the runstream. ' LAM y STO quotes the lam so STO ( ob lam → ) receives the name; drop the quote and you would hand it y’s contents instead.
kcUpArrow ?CaseKeyDef
::
LAM y DUP
BINT10 #<ITE
:: DROPZERO ERRBEEP ;
:: BINT10 #- ;
' LAM y STO
;Same shape, one structural difference: ' LAM y STO is now after the branch, shared by both paths — so both branches must leave a number behind. That is what DROPZERO ( ob → #0 ) is for: throw away y, push 0, beep. Shift-↑ near the top edge therefore slides you to the edge rather than refusing to move, which is the better behaviour and costs one word.
In the right-arrow version the else-branch is a bare #10+ with no :: wrapper at all. That is legal because ITE takes two objects, and a single word is one object. It is also, reliably, the line that stops people reading ROM listings.
A 19-pixel sprite on a 131 × 56 display fits while x ≤ 112 and y ≤ 37. Every limit in the program is one of those two numbers, tested one below:
Upy < 1 → beep · shifted y < 10 → 0Downy > 36 → beep · shifted y > 27 → 27Leftx < 1 → beep · shifted x < 10 → 0Rightx > 111 → beep · shifted x > 102 → 112“Beep when x > 111” lets x reach 112, and “beep when y > 36” lets y reach 37 — both exactly the last fitting position. The shifted Right clamp of 112 agrees.
The shifted Down clamp does not. BINT27 #>ITE :: DROP BINT27 ERRBEEP ; snaps y back to 27, so from anywhere in 28–37 a shift-↓ jumps the face upwards. The same action in the menu list two pages later clamps to 37, which is what the hard key should do. It is not in the published errata — it is simply the one line in the listing worth not copying.
{
{ "Up" {
:: (unshifted: one step)
LAM y DUP BINT1 #<ITE
:: DROP ERRBEEP ;
:: #1- ' LAM y STO ;
;
:: (left-shift: ten steps)
LAM y DUP BINT10 #<ITE
:: DROPZERO ERRBEEP ;
:: BINT10 #- ;
' LAM y STO
;
}
}
{ "Down" { … } }
{ "Left" { … } }
{ "Right" { … } }
NullMenuKey
{ "Quit" :: TRUE ' LAM exit? STO ; }
}A menu key is { label action }. When the action is itself a list, the list is indexed by plane: first entry unshifted, second left-shifted. The bodies are the hard-key handlers copied verbatim — which is why the listing is 195 lines for a 90-line program. NullMenuKey holds the empty fifth slot so Quit lands under the rightmost softkey.
Two things away from here make the menu work. DUP#<7 casedrpfls at the top of each plane returns FALSE for keycodes 1–6 — the softkeys — and the NonAppKeyOK? parameter is TRUE, so the loop falls back to the standard definition for them. The standard definition of a softkey is the menu entry. Set either of those the other way and the menu goes dead.
Quit does the only thing that ends the program: store TRUE into exit?.
TrueTrue (NonAppKeyOK? · DoStdKeys?)
{ … the menu … } (AppMenu)
ONEFALSE (first menu row, no suspended envs)
' LAM exit? (exit condition)
'ERRJMP (error handler)
ParOuterLoop (run the par outer loop)
RECLAIMDISP (resize and clear display)
ClrDAsOK (redraw display)
;ParOuterLoop wants nine arguments, in this order: AppDisplay · AppKeys · NonAppKeyOK? · DoStdKeys? · AppMenu · #AppMenuPage · SuspendOK? · ExitCond · AppError. Count the words in front of it and you find seven — because TrueTrue ( → T T ) and ONEFALSE ( → #1 F ) each push two. Fused constants like these are all over the ROM; recognising them is most of what makes ROM listings readable.
' LAM exit? is quoted for the same reason AppDisplay was: the loop wants the recall-object to evaluate before each key, not the flag’s value now. 'ERRJMP as AppError means “don’t handle it, re-raise” — ParOuterLoop wraps itself in ERRSET and has already restored the saved interface by the time your handler runs.
When ExitCond finally evaluates TRUE the loop returns, and the last two words give the screen back to the stack display.
' obpushes the object; without the quote it runs. Three of the nine loop parameters are quoted secondaries — miss one and you execute your own display code at setup time.case…every case word exits the whole secondary, not the clause. There is no “break”, and nothing after a matched case ever runs.ITE #<ITEtake the next two objects from the runstream. A bare #10+ is a complete branch; a :: … ; is one object too.bintscoordinates, counts and keycodes are bints, not reals: #1+, #-, #10+, #< — never the % words.GROB!bang words return nothing and edit in place. So do CLEARVDISP and DispMenu.1.FALSEfrom AppKeys means “not my key” — it is how you keep standard behaviour, not how you suppress it.no ABNDthe BIND on line 32 never gets a matching ABND. Chapter 18 is explicit that every BIND needs one and that nothing checks it for you. Add it before the final ; if you adapt this program.nothing checksCK0NOLASTWD is the only guard in 195 lines. That is the bargain: no seatbelts, and a program that redraws a sprite faster than User RPL can parse the request.Listing: Programming in System RPL, 2nd ed., Kalinowski & Dominik, §32.8 “An Example”, pp. 218–223; parameter descriptions from §32 and ch. 18, entry diagrams from the index above. Code shown in fragments, with the four near-identical arrow handlers and menu keys collapsed to one of each.