HP-41 Calculator Emulator in Rust: How to faithfully rebuild a classic
3 days. 8 phases. 13,399 lines of Rust. And a calculator from 1979 that once again feels just like the original.
The HP-41C: a legend
Anyone who was an engineer, scientist or pilot in the 1980s knows it: the HP-41C. Introduced by Hewlett-Packard in 1979, it was the world’s first alphanumeric, programmable calculator — expandable through modules, networkable via a special interface, and the calculating machine of choice for NASA astronauts.
The defining feature: RPN — Reverse Polish Notation. No equals sign. Instead, a 4-level stack on which operands are placed and computed directly. Anyone who has worked with it often never goes back.
My goal was to implement this calculator as a faithful emulation in Rust — not as a pixel clone, but as a behavioural emulation: whoever uses the emulator should feel as if they were using the original.
What «faithful» means
The hardest part was not the maths. It was understanding what exactly has to be emulated.
A calculator clone that computes 2 ENTER 2 × to 4 is trivial. But a faithful clone must also simulate the following correctly:
Stack-lift semantics: With the HP-41, each operation individually decides whether the stack is lifted before the next input. ENTER enables the lift. CLX disables it. + enables it. But STO is neutral. Implement it wrong, and the calculator behaves subtly wrong — hard to describe, but immediately noticeable.
ISG/DSE loops: The HP-41 uses a clever format for loop counters: CCCCC.FFFDD — a number in which the digits before the decimal point are the current value, the first three decimal places the target value, and the last two the step size. These fields must be extracted by string split — never with floor() or fmod() on f64, because floating-point rounding errors would destroy the fields.
EEX input: Scientific notation requires guards: no double decimal point, no E without a preceding mantissa, correct fallback paths between from_str() and from_scientific().
Architecture
The project is a Cargo workspace with two crates:
hp41-core/ — UI-agnostic library (calculation engine, no CLI dependencies)
hp41-cli/ — Terminal UI binary (ratatui + crossterm)
The strict separation is not a nice-to-have — it is a compile-time invariant. hp41-core must never depend on hp41-cli. This makes a future GUI possible (planned for v2.0 with Tauri) without changes to the core.
The CalcState
At its heart is CalcState in hp41-core/src/state.rs: a single data structure that holds the entire calculator state — stack, registers, flags, ALPHA mode, program memory, persistence metadata. All operations receive a &mut CalcState and return it. No global state, no mutex, no Arc.
The Op enum and dispatch()
Each operation is a variant of the Op enum. The dispatch() function in ops/mod.rs matches the Op and calls the corresponding implementation. Each Op declares its LiftEffect:
pub enum LiftEffect {
Enable, // ENTER, arithmetic results
Disable, // CLX, digit entry
Neutral, // STO, RCL
}
apply_lift_effect() in the stack takes care of the rest. The pattern makes the stack-lift semantics auditable: whoever adds a new operation must declare the LiftEffect explicitly.
Number arithmetic: rust_decimal
Instead of custom BCD we use rust_decimal — a library that represents decimal numbers exactly with 28 significant digits, with 10-digit rounding for HP-41 compatibility. Custom BCD was evaluated and rejected: too much maintenance effort for no measurable gain.
TUI with ratatui
The terminal UI is based on ratatui 0.30 with crossterm 0.29. An important detail: we use ratatui::init() instead of Terminal::new(), because the former automatically installs the panic hook that restores the terminal in the event of a crash.
Windows-specific: crossterm fires both KeyEventKind::Press and KeyEventKind::Release. Without a filter, every key would be executed twice.
Quality goals and results
From the outset, concrete, measurable goals were defined:
| Gate | Goal | Result |
|---|---|---|
| Cold start | ≤ 500 ms | 2.2 ms (228× margin) |
| Key latency | ≤ 50 ms | ~65 ns/op |
hp41-core coverage | ≥ 80 % | 94.87 % |
| Numerical accuracy | ≥ 98 % | 99 % (495/500) |
| Panics in hp41-core | 0 | 0 |
| CI platforms | Win/macOS/Ubuntu | All green |
The zero-panic goal is enforced at compile time: #![deny(clippy::unwrap_used)] in the crate root forbids every .unwrap() in production code. All errors are handled with ? propagation or .expect("justified").
Coverage is measured with cargo-llvm-cov. An important detail: just coverage first calls cargo llvm-cov clean --workspace to discard stale .profraw data from parallel worktree runs — otherwise the tool overestimates the coverage.
What I learned
1. Faithful emulation is 80 % documentation reverse-engineering. The original HP-41 manuals are precise, but terse. Many edge cases had to be reconstructed by combining the manual, community resources (hpmuseum.org) and trial and error.
2. Subtle behavioural bugs are worse than crashes. A crash is obvious. A stack lift that is wrong in 5 % of cases is barely findable — and ruins the «feel» of the calculator.
3. Architectural discipline pays off early. The strict core/CLI separation was set from the start and never called into question. That made testing, refactoring and the later extension (USER mode, persistence) considerably easier.
4. Property-based testing for stack invariants.
With proptest we test stack invariants over random inputs. That found several subtle stack-lift bugs that unit tests would not have covered.
Outlook: v1.1 and v2.0
v1.0 is the complete CLI implementation. What comes next:
- v1.1: UX improvements, STO arithmetic modals, EEX trailing-E fix
- v2.0: Tauri desktop app (
hp41-gui) with a graphical interface
Conclusion
Emulating vintage hardware is one of the most instructive exercises in software engineering. You cannot look it up on Stack Overflow. You have to read the specification, understand the behaviour, and then write Rust that is deterministic and correct.
The result: a calculator from 1979 that runs on any modern terminal — started in 2.2 ms, with 65 ns key latency, without a single panic.
The project is open source: github.com/talent-factory/hp41-calculator-emulator
git clone https://github.com/talent-factory/hp41-calculator-emulator.git
cd hp41-calculator-emulator
cargo install just
just run