HP-41 Calculator Emulator in Rust: Wie man einen Klassiker faithful nachbaut
3 Tage. 8 Phasen. 13'399 Zeilen Rust. Und ein Taschenrechner aus dem Jahr 1979, der sich wieder so anfühlt wie das Original.
Der HP-41C: Eine Legende
Wer in den 1980er Jahren Ingenieur, Wissenschaftler oder Pilot war, kennt ihn: den HP-41C. 1979 von Hewlett-Packard eingeführt, war er der erste alphanumerische, programmierbare Taschenrechner der Welt — erweiterbar durch Module, vernetzbar über ein spezielles Interface, und die bevorzugte Rechenmaschine der NASA-Astronauten.
Das charakteristische Merkmal: RPN — Reverse Polish Notation. Kein Gleichheitszeichen. Stattdessen ein 4-stufiger Stack, auf dem operanden abgelegt und direkt verrechnet werden. Wer einmal damit gearbeitet hat, kommt oft nie wieder zurück.
Mein Ziel war es, diesen Rechner als faithful Emulation in Rust umzusetzen — nicht als Pixel-Klon, sondern als Verhaltens-Emulation: Wer den Emulator benutzt, soll sich fühlen wie mit dem Original.
Was «faithful» bedeutet
Der schwierigste Teil war nicht die Mathematik. Es war das Verstehen, was genau emuliert werden muss.
Ein Taschenrechner-Klon, der 2 ENTER 2 × zu 4 ausrechnet, ist trivial. Aber ein faithful Klon muss auch folgendes korrekt simulieren:
Stack-Lift-Semantik: Beim HP-41 entscheidet jede Operation einzeln, ob der Stack vor der nächsten Eingabe angehoben wird. ENTER aktiviert den Lift. CLX deaktiviert ihn. + aktiviert ihn. Aber STO ist neutral. Falsch implementiert, und der Rechner verhält sich subtil falsch — schwer zu beschreiben, aber sofort spürbar.
ISG/DSE-Schleifen: Der HP-41 verwendet ein cleveres Format für Loop-Counter: CCCCC.FFFDD — eine Zahl, bei der die Stellen vor dem Punkt der aktuelle Wert sind, die ersten drei Nachkommastellen der Zielwert und die letzten zwei die Schrittweite. Diese Felder müssen per String-Split extrahiert werden — nie mit floor() oder fmod() auf f64, weil Gleitkomma-Rundungsfehler die Felder zerstören würden.
EEX-Eingabe: Die wissenschaftliche Notation erfordert Guards: kein doppeltes Dezimalkomma, kein E ohne vorhergehende Mantisse, korrekte Fallback-Pfade zwischen from_str() und from_scientific().
Architektur
Das Projekt ist ein Cargo-Workspace mit zwei Crates:
hp41-core/ — UI-agnostische Bibliothek (Rechenmotor, keine CLI-Abhängigkeiten)
hp41-cli/ — Terminal-UI Binary (ratatui + crossterm)
Die strikte Trennung ist kein Nice-to-have — sie ist eine compile-time Invariante. hp41-core darf nie von hp41-cli abhängen. Das ermöglicht künftig eine GUI (geplant für v2.0 mit Tauri) ohne Änderungen am Core.
Der CalcState
Herzstück ist CalcState in hp41-core/src/state.rs: eine einzige Datenstruktur, die den gesamten Rechner-Zustand hält — Stack, Register, Flags, ALPHA-Modus, Programmspeicher, Persistenz-Metadaten. Alle Operationen erhalten einen &mut CalcState und geben ihn zurück. Kein globaler State, kein Mutex, kein Arc.
Das Op-Enum und dispatch()
Jede Operation ist ein Variant des Op-Enums. Die dispatch()-Funktion in ops/mod.rs matcht den Op und ruft die entsprechende Implementierung auf. Jeder Op deklariert seinen LiftEffect:
pub enum LiftEffect {
Enable, // ENTER, arithmetic results
Disable, // CLX, digit entry
Neutral, // STO, RCL
}
apply_lift_effect() im Stack kümmert sich um den Rest. Das Muster macht die Stack-Lift-Semantik auditierbar: Wer eine neue Operation hinzufügt, muss den LiftEffect explizit deklarieren.
Zahlen-Arithmetik: rust_decimal
Statt Custom-BCD verwenden wir rust_decimal — eine Bibliothek, die Dezimalzahlen mit 28 signifikanten Stellen exakt darstellt, mit 10-Stellen-Rounding für HP-41-Kompatibilität. Custom-BCD wurde evaluiert und verworfen: zu viel Wartungsaufwand für keinen messbaren Gewinn.
TUI mit ratatui
Die Terminal-UI basiert auf ratatui 0.30 mit crossterm 0.29. Ein wichtiges Detail: Wir verwenden ratatui::init() statt Terminal::new(), weil ersteres automatisch den Panic-Hook installiert, der das Terminal bei einem Absturz wiederherstellt.
Windows-spezifisch: crossterm feuert sowohl KeyEventKind::Press als auch KeyEventKind::Release. Ohne Filter würde jede Taste doppelt ausgeführt.
Qualitätsziele und Ergebnisse
Von Anfang an waren konkrete, messbare Ziele definiert:
| Gate | Ziel | Ergebnis |
|---|---|---|
| Kaltstart | ≤ 500 ms | 2,2 ms (228× Marge) |
| Tastenlatenz | ≤ 50 ms | ~65 ns/op |
hp41-core Coverage | ≥ 80 % | 94,87 % |
| Numerische Genauigkeit | ≥ 98 % | 99 % (495/500) |
| Panics in hp41-core | 0 | 0 |
| CI-Plattformen | Win/macOS/Ubuntu | Alle grün |
Das Zero-Panic-Ziel ist compile-time erzwungen: #![deny(clippy::unwrap_used)] im Crate-Root verbietet jedes .unwrap() im Produktionscode. Alle Fehler werden mit ?-Propagation oder .expect("begründet") behandelt.
Coverage wird mit cargo-llvm-cov gemessen. Ein wichtiges Detail: just coverage ruft zuerst cargo llvm-cov clean --workspace auf, um veraltete .profraw-Daten aus parallelen Worktree-Runs zu verwerfen — sonst überschätzt das Tool die Coverage.
Was ich gelernt habe
1. Faithful Emulation ist zu 80 % Dokumentations-Reverse-Engineering. Die Original HP-41-Manuals sind präzise, aber knapp. Viele Edge Cases mussten durch Kombination von Handbuch, Community-Ressourcen (hpmuseum.org) und Trial-and-Error rekonstruiert werden.
2. Subtile Verhaltensfehler sind schlimmer als Abstürze. Ein Crash ist offensichtlich. Ein Stack-Lift, der in 5 % der Fälle falsch ist, ist kaum auffindbar — und ruiniert das «Gefühl» des Rechners.
3. Architektur-Disziplin zahlt sich früh aus. Die strikte Core/CLI-Trennung war von Anfang an gesetzt und nie in Frage gestellt. Das machte Testing, Refactoring und die spätere Erweiterung (USER mode, Persistence) deutlich einfacher.
4. Property-Based Testing für Stack-Invarianten.
Mit proptest testen wir Stack-Invarianten über Zufalls-Inputs. Das hat mehrere subtile Stack-Lift-Bugs gefunden, die Unit-Tests nicht abgedeckt hätten.
Ausblick: v1.1 und v2.0
v1.0 ist die vollständige CLI-Implementierung. Was als nächstes kommt:
- v1.1: UX-Verbesserungen, STO-Arithmetik-Modals, EEX-Trailing-E-Fix
- v2.0: Tauri-Desktop-App (
hp41-gui) mit grafischer Oberfläche
Fazit
Vintage-Hardware zu emulieren ist eine der lehrreichsten Übungen in Software-Engineering. Man kann nicht auf Stack Overflow nachschlagen. Man muss die Spezifikation lesen, das Verhalten verstehen und dann Rust schreiben, das deterministisch und korrekt ist.
Das Ergebnis: ein Taschenrechner aus 1979, der auf jedem modernen Terminal läuft — in 2,2 ms gestartet, mit 65 ns Tastenlatenz, ohne einen einzigen Panic.
Das Projekt ist 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