DB48X is a modern implementation of RPL (Reverse Polish Lisp) targeting SwissMicros calculators (DM32, DM42, DM42n). It's a complete calculator firmware written in C++ with:
- An RPL object system with compact LEB128 encoding
- Qt-based simulator for development/testing
- Firmware builds for ARM Cortex-M4 (DM42/DM32 hardware)
- Android and WebAssembly builds
- Do not stack unrelated code changes. If the user is in the middle of developing feature A, avoid introducing unrelated changes (B, C) into the same edits; they can be hard to deinterlace for clean commits. If the repository is clean and a git operation would make it easy to restore the earlier state, then making extra changes is OK. Otherwise, add ideas to the todo list to be done later in a separate commit.
- Commits must contain the user's Signed-off-by line in addition to the Co-authored-by mentioning the model. Do not invent a Signed-off-by address, when in doubt check earlier commits for that author in the history.
- Never push anything without being explicitly asked to for that commit A request to push for one commit is not an authorization to push later ones.
- Avoid overwriting the user's edits. When making changes, the user may have adjusted files themselves. Read the current file state before editing; prefer minimal, targeted edits that accomplish the goal without clobbering unrelated changes the user has made.
- Use
rg(ripgrep) instead offind/grepwhen searching. Thergcommand is faster and more battery-efficient thanfindorgrep. Examples:rg "pattern"instead ofgrep -r "pattern"rg -t cpp "pattern"to search only C++ filesrg -l "pattern"to list files (likegrep -l)rg --files | rg "\.h$"instead offind . -name "*.h"
- Do not add case-only aliases in
ids.tbl. Command parsing is case-independent, soLIMIT,Limit, andlimitall refer to the same command once it is registered. ReserveALIASfor spellings that differ in more than capitalization (e.g.TAYLRforTaylor, orPartialFractionsforPartFrac). Symbol names are also case-independent by default (IgnoreSymbolCase):Xandxare the same, as areABC,abc, andabC. Do not treat capitalization as a distinct variable in docs, tests, or debugging. This does not apply toⓧ(VX): that object names the current algebra variable (STOVX/RCLVX), defaults tox, and after'ABC' STOVXdenotesABC— notx/X.
Simulator (for development/testing):
make -j sim # Build Qt simulator
./db48x.app/Contents/MacOS/db48x # Run on macOS
./db48x # Run on LinuxFirmware:
make -j8 # Build DM42 firmware (db48x.pgm)
make -j8 debug # Debug build
make -j8 dm32 # Build for DM32 (db50x.pg5)QSPI CRC rebuilds: If you see "Run build once more", run make again. QSPI is flash memory containing numerical routines;
the build forces CRC compatibility with original DM42 firmware to allow
switching between DB48X and stock DM42
firmware.- help / db48x.md and help / db50x.md are **auto -
generated **by the build.Never edit them directly;
edit doc / commands/*.md instead.
- Documentation internal links: use only the anchor part, e.g.
(#Anchor), not(settings.md#Anchor). The shipped help (db48x.md,db50x.md) is built by concatenating and sed-editing the input markdown files into a single file, so cross-file links break. - Command cross-references in docs: to reference another command, simply use
backtick syntax, e.g.
`ToRectangular`. The help rendering engine automatically links backtick-quoted command names to their documentation. Any spelling or alias works; the engine displays the current style. Do not use explicit markdown links for command cross-references. - No duplicate entries for alternate spellings: the help system automatically
displays all alternate spellings for a command. Do not create separate
##entries for aliases or compatibility spellings. Document each command once under its primary name. - No markdown bold for emphasis in command docs: do not write
**foo**indoc/commands/*.mdto highlight words. The built-in help renderer does not display bold correctly, and it is easy to overuse. Use backticks for command names, settings, and literal RPL fragments; otherwise write plain prose.
src/ids.tbl: AddNAMED(Foo, "→Foo")orCMD(Foo). UseALIASonly for spellings that differ in more than case (see general guideline above). PreferNAMED(ShortName, "LongSpelling")when the C++ identifier is short (e.g.NAMED(PRoot, "PolynomialRoots")). Update anyID_RANGElines whose boundary the new command changes (e.g.is_command,is_algebraic_fn).- Header (e.g.
src/functions.h): Declare withFUNCTION(Foo),COMMAND_DECLARE(Foo, nargs),COMMAND_DECLARE_FN(Foo, nargs), orSTANDARD_FUNCTION(Foo)as appropriate (see below). - Implementation (e.g.
src/functions.cc): Implement withFUNCTION_BODY(Foo)orCOMMAND_BODY(Foo). src/menu.cc: Wire up menu entries (replaceID_Unimplementedif a placeholder exists).
Commands that should be usable in algebraic expressions (like 'JDN(DateTime)')
must use COMMAND_DECLARE_FN instead of COMMAND_DECLARE:
COMMAND_DECLARE_FN(name, arity)addsPREC_DECL(FUNCTION)for function precedence- Makes the command parseable and callable inside algebraic expressions
- Use for: mathematical functions, conversion functions, any fixed-arity command that returns one result and should work in expressions
Note: SYMARGS and SYMARGS_DECL are separate and only needed for
functions that require arguments to stay symbolic/unevaluated (like Sum,
Integrate, Quote). Most functions just need COMMAND_DECLARE_FN, not
SYMARGS.
Example:
// In header (src/datetime.h):
COMMAND_DECLARE_FN(JulianDayNumber, 1); // Can be used in expressionsThen in RPL:
@ Works in both RPN and algebraic mode:
2461178.5 JDN→ @ RPN: OK
'JDN→(2461178.5)' @ Algebraic: OK (see commit 40c59d837)
Variable arity functions (like ToRange which takes 1 or 2 arguments) use
negative arity notation:
COMMAND_DECLARE_FN(ToRange, ~2); // ~2 means "at least 2, variable arity"During expression parsing, variadic functions are wrapped in funcall to handle
argument count correctly. The infrastructure provides variable_arity() and
arity() helpers.
- Menus display 6×3 = 18 entries when there are fewer than 18; otherwise 5×3 + next/previous keys (15 visible, paginated).
- Group entries logically (vertically or horizontally). Alignment matters for usability.
- When adding a menu entry, ensure the count stays ≤18 or that the grouping still makes sense with pagination. If adding one entry would exceed 18, consider removing another that is available elsewhere (e.g. via shortcuts or other menus).
- Build the simulator first:
make -j sim - Quick RPL checks (no rebuild): use simulator
-Eto evaluate a command line and print the stack, e.g.db48x -H -E '[1 2 3] PROOT'. Read from stdin with-F -, e.g.db48x -H -F - -E '1 2 +'. SeeSIMULATOR.md.
Quick testing examples from the test suite:
Using simulator -E flag for quick RPL checks:
./db48x.app/Contents/MacOS/db48x -H -E "Clear 'JDN(20250919_date)' →Num"Getting default RPL spelling for commands
To get the default RPL spelling for commands with aliases, you can use the following trick:
db48x -H -E "{ ToPolar PolarToReal RealToPolar }"This produces a list containing the actual spellings after parsing:
{ →Polar Polarℂ→ℝ ℝ→Polarℂ }
- Before submitting changes, run the full test suite and fix any failures.
From the repository root (after building the simulator):
The
db48x -H -Tall-Hflag runs in headless mode (no Qt window). Exit code 0 means all tests passed; non-zero means there are failures (see console output orfailures.logif you capture it). - Treat every test failure as your problem until proven otherwise. The user
typically invokes you with a clean test suite. Do not dismiss failures as
“pre-existing”, “unrelated”, or “environmental” unless you have proven
that (e.g. reproduced on the parent commit, or isolated the failure to code
you did not touch). Failures in tests that run after your new tests are
often caused by state leakage (settings, reserved variables such as
PPAR, directories, flags). Investigate before claiming innocence. - To run a single test group:
db48x -H -Tname(e.g.-Tsectrig). - Enable recorder traces during tests with
-tchannel(see Debugging section):db48x -H -Ttoqpi -talgebraic - All new features and bug fixes must include tests. Build and run the
full suite (
db48x -H -Tall) before considering work complete.
Platform notes:
- Run simulator from repo root so
help/andconfig/directories are found - DMCP: SwissMicros Device Management Control Program (calculator firmware platform)
- QSPI: Quad-SPI flash region storing numerical routines
- Test functions go in
src/tests.cc, declared insrc/tests.h. - Every test function using
BEGIN(name)must have a correspondingTESTS(name, "description")macro near the top oftests.cc(around lines 89-160). Without it, thecheck_<name>function won't exist and the sim build will fail. - Register the function call in the test runner (around line 237).
- The calculator works at 24-digit precision by default. Test inputs typed as
literal decimals must have enough digits, or use computed values (e.g.
"pi →Num 2 /"instead of"1.5707963268"). - Default fraction display uses superscript/subscript (
¹/₄). Use"BigFractions ImproperFractions"in tests for plain1/4format, and restore with"SmallFractions MixedFractions"at the end. - Restoring settings: when a test changes a setting (e.g.
10000 FractionLargestPrime), restore the default at the end usingPurge:{ FractionLargestPrime } Purge Std. TheStdreapplies standard defaults. SeeMaxFactorIterationsandFractionLargestPrimeintests.ccfor examples. - Sandbox directories: when a test modifies reserved variables, global
state, or directory contents that later tests depend on, run it inside a
temporary directory:
'MyTest' CRDIR, enter it, run the test, thenUPDIRand'MyTest' PURGE. SeeSLVTSTinsolver_testing,ExamplesTestincheck_help_examples, andPlotIndepTestinplottingfor examples. Plotting tests that changePPARmust not run in the main directory unless the prior state is fully restored before subsequent image-based plot tests. - Test assertions (
.expect,.want,.match):- Use
.expect()for exact match. - Use
.want()when you only need to ignore/allow variation in spaces (e.g. matrices and vectors, which may include newlines or extra spaces). - Use
.match()when the expected value is easier to express with a regexp.
- Use
- Vectors and matrices cannot contain non-algebraic expressions directly.
Use
[ 0.25 '√2' ] →Numinstead of[ 0.25 2 √ →Num ]when you need a vector or matrix of decimals for testing. - Lists are not evaluated in RPL.
{ 1.5 neg 2.3 }builds a list containing the literal objects 1.5,neg(the operator), and 2.3 — not -1.5. For a list with negative numbers, use{ -1.5 2.3 }(literal minus). Usingneginside a list leads to "Bad argument type" when applying →Q or similar commands that expect numeric elements.
User-visible tests Document RPL commands and functions in the topical
markdown files under doc/. This is introduced with an RPL block that looks
like this and is automatically scanned by the test suite
1 2 +
@ Expecting 3
See tests::check_help_examples() for the code that scans and evaluates all
these code snippets. Avoid comments in the code snippets since they are part of the documentation and can be copy/pasted by the user. Usually, the comments should go above the RPL code snippet, i.e. write something like this:
Adding integers produces the result on the stack:
```rpl
1 2 +
@ Expecting 3
```
rather than
```rpl
@ Adding integers produces the result on the stack:
1 2 +
@ Expecting 3
```
Remember that RPL uses @ for comments, not // or #
The @ Expecting fragments needs the space between @ and Expecting
The test suite cleans up between RPL code snippets, except if they contain a
comment that begins with @ Keep or @ Save, in which case the variables and
other changes made by the code snippet are visible to the next one.
-
DB48X uses C++ in a very special way. Object values are not stored in the object itself, but as LEB128 encoding in the object. RPL objects can be moved in memory, garbage collected, etc. They are byte-aligned and very compact. It is NOT ALLOWED EVER EVER EVER to use a virtual function, because that would add at least one word to the object for the vptr, which can't be copied or moved willy-nilly. Dispatch is done using
obj->type()and a switch statement (or the handler table), not virtual functions. -
For the same reason, it is NOT OK TO USE THE STANDARD LIBRARY, for example
std::vector, because on device firmware there is no realmalloc(), no ecception handling, etc. Usingstd::vectorresults in unsatisfied symbols building the firmware, and the correct approach is to avoid usingstd::vector, not to try random disastrous repairs to bring the firmware up to standard. -
src/ids.tblis the single source of truth for all object/command IDs. It is#included with varying macro definitions to generate enums, spelling tables, and dispatch tables. -
Promote reusable helpers to shared modules: do not multiply local
statichelpers when an existing shared utility would do better. Before adding local helpers, check the relevant subsystem (runtime.*,complex.*,factor.*, etc.). If the logic is generic and reused, move it to the appropriate shared file and call it from feature code. -
Dispatch:
object::handler[ID_Foo].evaluatecallsFoo::do_evaluate. -
For
FUNCTION-style commands: the evaluate chain isdo_evaluate->evaluate()(static) ->evaluate(algebraic_r x). -
Smart pointer conventions (
_p,_g,_r):- Prefer
_preturn types unless there is a type-related need for_g(e.g. the caller must extend lifetime / adopt a new allocation). - Use
_rfor arguments unless the callee would need to construct a_ginside (e.g. to hold a temporary). _g= GC-safe owning pointer;_p= non-owning raw pointer;_r= reference.
- Prefer
-
Arithmetic operators (
+,-,*,/) are overloaded foralgebraic_r. Multiplying a numeric type by a symbolic type (e.g.constant) will typically try numeric evaluation, not create a symbolic expression. Useexpression::make(ID_multiply, x, y)to build symbolic products explicitly. -
constant::lookup("π", false)returns a symbolic constant (thefalsesuppresses errors if not found).algebraic::pi()returns a numeric value.
The cleaner purge pattern is a lightweight memory reclamation technique that complements garbage collection. It reclaims intermediate temporaries without running a full GC cycle.
Usage pattern:
cleaner purge; // Save current Temporaries pointer
algebraic_g result = evaluate(op, y, x); // Do work (may create temps)
if (+result != +x && +result != +y) // Check if result is new
result = purge(result); // Purge intermediate temporaries
return result;How it works:
cleaner purge;saves the currentrt.Temporariespointer and GC cycle count- During computation, intermediate temporary objects may be allocated above that mark
purge(result)checks if:- No GC ran during the operation (cycle count unchanged)
- The result is a new temporary (above the saved mark)
- Automatic cleanup is enabled in settings
- If all conditions hold, it discards all temporaries between the saved mark and the result, keeping only the result by moving it down to the saved position
- If GC ran (objects may have moved),
purge()returns the result unchanged
When to use:
- At the end of functions that create multiple intermediate objects but return one final result
- The idiom is
cleaner purge;at function start, thenpurge(result)before returning
The input check: Always verify the result is new before purging:
if (+result != +input1 && +result != +input2)
result = purge(result);If you skip this check and the result aliases an input still on the stack, purging would move that stack object, corrupting the stack.
Statistics: The runtime tracks both memory reclamation mechanisms separately:
rt.GCPurged- bytes reclaimed by full garbage collectionrt.GCCleared- bytes reclaimed by cleaner purge operations
The ID_RANGE(is_integer, ...) range covers IDs from based_integer through
neg_integer. Because of the ordering in ids.tbl, bignums fall inside this
range (bignum and neg_bignum have IDs between based_integer and
neg_integer). Therefore object::is_integer(ID_bignum) returns true.
When extracting numeric values, always check is_bignum before is_integer
and use bignum_p(…)->value<ularge>(). Casting a bignum via integer_p gives
garbage because integer uses LEB128 encoding while bignum inherits from
text (length-prefixed raw bytes). Similarly, is_fraction includes
big_fraction, so fraction_p(…)->numerator_value() works for both, but be
aware of the overlap.
The general rule: narrow type checks (is_bignum, is_big_fraction) before
wide ones (is_integer, is_fraction) when the code path depends on the
memory layout.
decimal::to_fraction() returns a bignum (not integer) for whole-number
results, because it works with bignum_g internally.
Use the project's recorder library instead of fprintf(stderr, ...).
- Each source file declares recorder channels near the top:
RECORDER(algebraic, 16, "RPL Algebraics"); - Log with
record(channel, "format %+s %u %p", ...):%+sforutf8/object::name(ty)strings%tfor readable object dumps (objects rendered like the stack display)%u,%dfor integers,%pfor pointers
- Recorder output is ring-buffered and low-overhead; safe to leave in
production code (unlike
fprintf). Never usefprintffor debugging. - Enabling traces at runtime: use the
-tflag on the simulator command line. The flag takes a comma-separated list of recorder channel names:You can also use the./db48x.app/Contents/MacOS/db48x -Ttoqpi -talgebraic ./db48x.app/Contents/MacOS/db48x -Tall -talgebraic,fraction,expressionRECORDER_TRACESenvironment variable:RECORDER_TRACES=algebraic ./db48x ... - Trace output is interleaved with test results on stderr, formatted as:
[sequence timestamp] channel: message - For new functionality, add
record()calls at key decision points (type dispatch, computed values, best-candidate selection) so future debugging doesn't require adding and removingfprintfstatements.
- Section comments (grouping multiple functions/structs): use
// ====on 5 lines:// ============================================================================ // // Section title // // ============================================================================ - Function/struct headers:
// ---...---separator lines around bodies. - One-line header only: The comment between the first
// ----lines after a function name must be strictly one line. If it can't be summarized in one line, it's too verbose. Put details below the second// ----line. - No narrating comments; comments explain non-obvious intent only.
- Use
object::ID_foo(not bareID_foo) in static/non-member functions where theidtypedef is not in scope. - No hanging statements: every
for,while,if, etc. that controls more than a trivial single statement must use braces. Even when the body is a single sub-statement (e.g. awhileinside afor), wrap the outer loop body in braces. Allman-style brace placement (opening brace on its own line). - Line length: if a statement fits within 80 columns, keep it on one line; avoid gratuitous line breaks.
- When unsure how to format a section of code, run it through
clang-format.