# Requirements: Tally - Cross-Platform Calculator ## Overview A calculator application with three frontends (CLI, TUI, Android) sharing a common Zig engine. Supports standard arithmetic, a programmer mode with struct layout visualization, and financial calculations. --- ## Functional Requirements ### FR-1: Standard Calculation Mode - **FR-1.1**: Parse and evaluate infix mathematical expressions with correct operator precedence (PEMDAS). - **FR-1.2**: Support operators: `+`, `-`, `*`, `/`, `%` (modulo), `^` (power), unary `-`. - **FR-1.3**: Support parentheses for grouping. - **FR-1.4**: Support built-in functions: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `log` (base-10), `ln` (natural), `sqrt`, `cbrt`, `abs`, `ceil`, `floor`, `round`, `factorial`. - **FR-1.5**: Support constants: `pi`, `e`, `tau`. - **FR-1.6**: Support variable storage (Ans for last result, named variables A-F, X, Y, Z). - **FR-1.7**: Maintain calculation history with replay capability. - **FR-1.8**: Commas accepted as digit separators in input (e.g., `1,000 * 2` = 2000). Spaces in hex literals treated as byte separators (e.g., `0xFF FF` = 0xFFFF). - **FR-1.9**: When a standard-mode expression contains any non-decimal literal (hex `0x`, octal `0o`, or binary `0b`) and the result is a non-negative integer, enrich the result display with hex/octal/binary representations inline (without leaving standard mode). Uses the smallest standard bit width (8/16/32/64/128) that holds the value. This does not change the evaluation semantics (still f64 arithmetic, `^` is still power); it only augments the display. Fractional or negative results show decimal only. ### FR-2: Programmer Mode - **FR-2.1**: Accept input in decimal, hexadecimal (`0x`), octal (`0o`), and binary (`0b`) formats. - **FR-2.2**: Simultaneously display results in all four bases (dec, hex, oct, bin). - **FR-2.3**: Support configurable bit widths: 8, 16, 32, 64, 128-bit. - **FR-2.4**: Support bitwise operators: AND (`&` or `and`), OR (`|` or `or`), XOR (`xor` keyword), NOT (`~` or `not`), left shift (`<<`), right shift (logical `>>>`), arithmetic right shift (`>>`), rotate left (`rol`), rotate right (`ror`). Note: `^` is always exponentiation (never XOR) - see FR-2.12. - **FR-2.12**: The `^` operator means exponentiation in all modes (never XOR). This avoids mode-dependent operator overloading. XOR is available only via the `xor` keyword. Power is also available via `**`. This keeps every operator's meaning identical across standard and programmer modes. - **FR-2.5**: Display both signed (two's complement) and unsigned interpretations of the current value. - **FR-2.6**: Visualize the bit pattern as a grid (integer.exposed style) - bits individually addressable/toggleable in TUI and Android. - **FR-2.7**: Quick toggle between base display formats via dedicated keyboard shortcuts (TUI). - **FR-2.8**: Configurable endianness display (little-endian default, big-endian available). Affects byte order in HEX display and byte-level visualizations. Does not change the underlying value - purely a display/interpretation toggle. - **FR-2.9**: ASCII/text literal input via single-quoted strings: `'hello'` packs ASCII bytes into the integer value. First character occupies the most significant used byte (big-endian packing when display is BE) or least significant byte (when display is LE). Useful for examining magic numbers, file signatures, protocol headers. - **FR-2.10**: ASCII interpretation display - when the current value contains printable ASCII bytes, show the text representation alongside the numeric bases (e.g., `ASCII: "ELF."` or `ASCII: ..lf` with dots for non-printable bytes). - **FR-2.11**: IEEE 754 floating-point interpretation (float.exposed style): - Support float32 (single) and float64 (double) formats, selectable - Decompose bit pattern into sign bit, exponent field, and significand/mantissa field with visual color-coding in the bit grid - Display the evaluation formula: (-1)^sign * 2^(exponent - bias) * 1.mantissa - Display the exact decimal value represented by the bit pattern - Display delta to next and previous representable float (ULP - unit in last place) - Handle special values: +/-infinity, NaN, denormals/subnormals, +/-zero - Allow toggling individual bits and seeing the float value update in real-time - Allow entering a decimal float value and seeing the closest IEEE 754 representation - Show when a value is not exactly representable (rounding occurred) ### FR-3: Struct Layout Visualizer (Programmer Mode Extension) - **FR-3.1**: Accept struct definitions in a mini-DSL: ``` struct { u32 x; u8 flags; u16 id; u64 timestamp; } ``` - **FR-3.2**: Compute and display total struct size in bytes. - **FR-3.3**: Display per-field information: offset, size, alignment requirement. - **FR-3.4**: Identify and highlight padding bytes between fields and at the end (tail padding). - **FR-3.5**: Render a byte-level memory map showing which bytes belong to which field and which are padding. - **FR-3.6**: Default to x86-64 System V ABI alignment rules. - **FR-3.7**: Support switching ABI/alignment profiles (extensible design - future: Windows x64, ARM AAPCS, packed). - **FR-3.8**: Support switching endianness display (little-endian default, big-endian available). - **FR-3.9**: Supported field types (initial set): `u8`, `u16`, `u32`, `u64`, `i8`, `i16`, `i32`, `i64`, `f32`, `f64`, `bool`, `ptr` (pointer-sized, arch-dependent). - **FR-3.10**: Show a "packed" comparison - display what the struct would look like with no padding vs. natural alignment. ### FR-4: Unit Conversions - **FR-4.1**: Support inline unit conversion syntax: ` to|in ` (e.g., `100 km to mi`, `32F in C`). IMPLEMENTED as ordinary expression input, so no subcommand is needed: `tally 32F in C` works, as does typing it in the TUI standard mode. The value may itself be an expression (`2*3 kg to lb`). - **FR-4.2**: Support unit conversions within expressions: `5 kg + 3 lb` evaluates in the left-hand unit. NOT YET IMPLEMENTED (needs unit-aware tokenizing and evaluation, unlike FR-4.1 which only needs the `to` keyword as a trigger). - **FR-4.3**: Length: nm, um, mm, cm, m, km, in, ft, yd, mi, nmi (nautical mile), ly, au, pc. Base unit: m. - **FR-4.4**: Mass/Weight: mg, g, kg, t (tonne), oz, lb, st (stone), ton (US short), lt (long ton). Base unit: kg. - **FR-4.5**: Temperature: C (Celsius), F (Fahrenheit), K (Kelvin), R (Rankine). Base unit: C. These are the only affine conversions (they need an offset, not just a factor). - **FR-4.6**: Time: ns, us, ms, s, min, h, d, wk, yr. Base unit: s. A year is the Julian year (365.25 days), the usual unit-conversion convention. - **FR-4.7**: Digital storage: bit, B, and both decimal (kB, MB, GB, TB, PB) and binary (KiB, MiB, GiB, TiB, PiB) prefixes, kept distinct. Base unit: B. - **FR-4.8**: Speed: m/s, km/h, mph, ft/s, kn (knots), c (speed of light). Base unit: m/s. - **FR-4.9**: Area: mm2, cm2, m2, km2, in2, ft2, yd2, mi2, ha, acre. Base unit: m2. - **FR-4.10**: Volume: mL, L, m3, cm3, in3, ft3, gal, qt, pt, cup, floz, tbsp, tsp, bbl. Base unit: L. - **FR-4.11**: Energy: J, kJ, cal, kcal, Wh, kWh, BTU, eV, erg. Base unit: J. - **FR-4.12**: Pressure: Pa, kPa, MPa, bar, mbar, atm, psi, torr, mmHg, inHg. Base unit: Pa. - **FR-4.13**: Data rate: bps, kbps, Mbps, Gbps, Tbps, Bps, kBps, MBps, GBps. Base unit: bps. - **FR-4.14**: Angle: rad, mrad, deg, grad, turn, arcmin, arcsec. Base unit: rad. - **FR-4.15**: In CLI, unit conversion is plain expression input (FR-4.1): `tally 100 km to mi`, `tally 32F to C`. A `convert` subcommand is also accepted for explicitness and scripting (`tally convert 100 km mi`, with or without the `to`), but it is a convenience, not the only route. - **FR-4.16**: In TUI, a dedicated convert mode reachable from the mode tabs. - **FR-4.17**: Unit system extensible - adding a unit or category requires only a table entry, no parser changes. - **FR-4.18**: Unit names resolve by canonical name or alias (long forms and plurals). An exact case-sensitive match is tried first so case-distinguished units keep their meaning (`K` is Kelvin, `B` is byte, `kB` is not `KiB`); a case-insensitive pass runs only as a fallback, so forgiving input like `KM` or `Celsius` still works. - **FR-4.19**: Converting between units of different categories is an error (`IncompatibleUnits`); an unrecognized unit name is an error (`UnknownUnit`). - **FR-4.20**: Conversion detection must not change the meaning of ordinary expressions. Only a standalone whitespace-delimited `to` or `in` triggers it, and a unit name is only recognized at a token boundary (so the trailing `s` of `smoots` is not read as seconds, and the `in` inside `min` is not read as a separator). Without a separator, `e` remains Euler's number rather than becoming a unit. - **FR-4.21**: Because `in` is both a separator and the canonical name for inches, candidate separators are tried last-to-first and the first reading that yields a valid conversion wins. This must resolve all of: `5 in in cm` (5 inches to cm), `100 mm in in` (mm to inches), `1 in in in` (identity), and `60 min in h` (minutes to hours). ### FR-5: Financial Mode - **FR-5.1**: Compute CAGR given start value, end value, and number of periods. - **FR-5.2**: Compute compound interest (future value given principal, rate, periods, compounding frequency). - **FR-5.3**: Compute present value (discount future cash flow). - **FR-5.4**: Support TVM (Time Value of Money) solver - given any 4 of: N, I/Y, PV, PMT, FV, solve for the 5th. - **FR-5.5**: Display intermediate calculation steps/formula used (transparency). ### FR-6: CLI Frontend - **FR-6.1**: Invoke as `tally ""` for standard mode evaluation. - **FR-6.2**: Flag `--programmer` or `-p` for programmer mode: `tally -p "0xFF & 0x0F"`. - **FR-6.3**: Flag `--bits <8|16|32|64>` to set bit width in programmer mode (default: 64). - **FR-6.4**: Flag `--base ` to control primary output format (default: show all). - **FR-6.5**: Subcommand `tally struct ` for struct layout - accepts inline DSL string or path to a file containing the definition. - **FR-6.6**: Subcommand `tally cagr ` for quick CAGR. - **FR-6.7**: Subcommand `tally tvm` with named flags (`--pv`, `--fv`, `--n`, `--rate`, `--pmt`) - omit one to solve for it. - **FR-6.8**: Subcommand `tally convert ` for unit conversion. - **FR-6.9**: Output format flags: `--json` for machine-readable output, plain text default. - **FR-6.10**: Exit code 0 on success, non-zero on parse/evaluation errors with stderr message. ### FR-7: TUI Frontend - **FR-7.1**: Full-screen terminal UI with mode tabs (Standard, Programmer, Financial, Convert). - **FR-7.2**: Standard mode: expression input line, result display, scrollable history. - **FR-7.3**: Programmer mode: bit grid (navigable with arrow keys, toggle with Space/Enter), simultaneous base displays, expression input. - **FR-7.4**: Programmer mode struct sub-view: field list editor, live-updating memory map visualization. - **FR-7.5**: Financial mode: form-style input for parameters, result display with formula breakdown. - **FR-7.6**: Convert mode: select category, input value, select from/to units, live result. - **FR-7.7**: Every action must be reachable from the keyboard alone; the TUI is fully usable without a mouse. - **FR-7.8**: Quick-switch keys for base display in programmer mode (e.g., `d`=dec, `h`=hex, `o`=oct, `b`=bin to highlight primary). - **FR-7.9**: Support terminal resize gracefully. - **FR-7.10**: Vi-style and Emacs-style keybinding options for expression input. #### FR-7.11: Mouse Support Mouse input is a first-class way to drive the TUI, not an optional extra. Anything visibly interactive must respond to a left click. Keyboard parity is still required (FR-7.7): the mouse never becomes the only way to do something. - **FR-7.11.1**: Clicking a mode tab switches to that mode. - **FR-7.11.2**: Clicking a bit in the programmer bit grid flips that bit and moves the cursor to it. - **FR-7.11.3**: Clicking a digit in the HEX or OCT row focuses that field and places the cursor on the exact digit clicked, ready for typing. A click does not alter the value, because a hex nibble or octal digit has no single unambiguous "toggle". - **FR-7.11.4**: Clicking a digit in the BIN row flips that bit, since one binary digit is exactly one bit. - **FR-7.11.5**: Clicking a DEC(s) or DEC(u) row focuses that field for editing. - **FR-7.11.6**: Clicking the `Bits:`, `Signed:`, or `Endian:` label in the programmer header toggles or cycles that setting. - **FR-7.11.7**: In the float view, clicking a bit flips it; clicking the format or exit hint in the header toggles f32/f64 or leaves the overlay. - **FR-7.11.8**: In convert mode, clicking a category chip selects that category (resetting the unit pair to that category's defaults); clicking a unit in the From or To column selects it. - **FR-7.11.9**: Clicking the input line returns keyboard focus to the expression prompt. - **FR-7.11.10**: Clicking anywhere dismisses the help overlay, matching its "any key dismisses" keyboard behavior. - **FR-7.11.11**: Only a left button press acts. Release, motion, and drag events are ignored so a single click cannot fire an action twice. - **FR-7.11.12**: Clickable regions are rebuilt every frame from what was actually drawn, so hit targets can never drift out of sync with the display. ### FR-8: Android Frontend - **FR-8.1**: Native Android app using Kotlin and Jetpack Compose. - **FR-8.2**: Call into the Zig engine via JNI (C ABI shared library). - **FR-8.3**: Bottom navigation bar for mode switching (Standard, Programmer, Financial, Convert). - **FR-8.4**: Standard mode: traditional calculator layout with button grid, expression display at top, result below. - **FR-8.5**: Programmer mode: tappable bit grid, segmented control for bit width, base display cards, operator button row for bitwise ops. - **FR-8.6**: Programmer mode struct sub-screen: text field for struct input, scrollable field table, color-coded memory map rendered as a custom Canvas composable. - **FR-8.7**: Financial mode: card-based UI with input fields, dropdown for function selection (CAGR, TVM, etc.), compute button, result card with formula. - **FR-8.8**: Convert mode: category picker (horizontal chips), from/to unit selectors, live conversion as user types. - **FR-8.9**: Responsive layout for phone and tablet (multi-pane on tablet). - **FR-8.10**: Follow Material Design 3 guidelines with dynamic color (Android 12+). - **FR-8.11**: Haptic feedback on bit toggle and button presses. - **FR-8.12**: Support landscape orientation with adapted layouts (wider bit grid, side-by-side panels). --- ## Non-Functional Requirements ### NFR-1: Performance - Expression parsing and evaluation must complete in < 1ms for typical expressions. - TUI must render at >= 30fps during interaction (bit toggling, navigation). - Struct layout computation must handle structs with up to 256 fields without perceptible delay. ### NFR-2: Portability - Engine, CLI, and TUI must compile and run on: Linux (x86-64, aarch64), macOS (aarch64, x86-64), Windows (x86-64). - Android engine .so must target: arm64-v8a, armeabi-v7a, x86_64. - Single `zig build` invocation to produce all desktop targets (cross-compilation). ### NFR-3: Correctness - Floating-point arithmetic uses IEEE 754 double precision (64-bit). - Programmer mode integer operations must be exact (no floating-point). - Struct layout computation must match the actual layout produced by a C compiler under the specified ABI. ### NFR-4: Extensibility - Adding a new ABI profile for struct layout requires implementing a single interface (alignment rules + pointer size). - Adding new financial functions requires no changes to the parser - use a function-call syntax. - Adding new unit categories/units requires only table entries, no parser changes. - Engine exposes a stable C ABI for future frontends (e.g., WASM, iOS). ### NFR-5: Testability - Engine must be testable in isolation (no I/O dependencies). - Struct layout results must be verifiable against `pahole` or compiler output for reference structs. - Engine test coverage target: >= 80% line coverage. The engine is pure computation with no I/O, so this is achievable and expected. - Frontend coverage: best-effort via integration and smoke tests (TUI key-sequence tests, Android instrumented tests). No hard coverage target on frontend code. - All public engine API functions must have corresponding unit tests covering happy path, error cases, and edge cases. ### NFR-6: Build & Distribution - **Zig build system for everything.** A single `zig build` invocation at the workspace root handles all targets: engine, CLI, TUI, and Android .so cross-compilation. No Gradle required for the native library build - Gradle only wraps the prebuilt .so into the APK. - **Mise for toolchain management.** All required toolchains (Zig version, Android NDK if needed for validation) are declared in `.mise.toml` at the project root. A developer needs only `mise` installed - running `mise install` provisions everything else. - **No other system-level dependencies.** Outside of mise, a developer should not need to manually install Zig, Android SDK/NDK, or any other toolchain. The `.mise.toml` is the single source of truth for tool versions. - **libvaxis for TUI.** The TUI frontend uses libvaxis (pulled as a Zig build dependency). No other TUI framework. - Produce statically-linked binaries for CLI and TUI (no shared library dependencies on end-user system). - Android: produce .so files for arm64-v8a, x86_64 via `zig build -Dtarget=aarch64-linux-android` (or similar). The Gradle project in `android/` consumes these prebuilt artifacts. - Minimal runtime dependencies: Zig stdlib + libvaxis (TUI only). No other native deps. ### NFR-7: Number Display & Formatting - Decimal numbers must use comma grouping for display (e.g., `4,294,967,295`). - Scientific notation only when absolute value > 10^15 / < 10^-15. Never jump to scientific notation for values that fit in a readable decimal. (An earlier draft also triggered scientific notation past 15 significant digits. That clause was never implemented and was wrong: read literally it renders `0.9999999999999998` as `9.999999999999998e-1`, which is worse. Superseded by NFR-9.) - Programmer mode hex values display with space-separated bytes (e.g., `FF FF FF FF`). - Programmer mode binary values display grouped by nibble with spaces (e.g., `1111 1111`). - All frontends must distinguish between "display format" (with separators) and "clipboard format" (raw, no separators). - Android: long-press any result to copy the raw (no-separator) value to clipboard. - TUI: yank command copies raw value to system clipboard. - CLI: stdout output uses display format by default; `--raw` flag or `--json` gives unformatted values. ### NFR-8: Security - C API must not use null-terminated strings. All string inputs take `(pointer, length)` pairs to prevent buffer overread. - Engine must validate all input lengths before processing. - No unbounded allocations from untrusted input (enforce maximum expression length, maximum struct field count). ### NFR-9: Numeric Correctness (Exact Arithmetic) Correctness is prioritized over evaluation speed. A calculator evaluates one expression at a time in response to a human, so there is no hot-loop or real-time constraint that justifies accepting wrong answers. - **NFR-9.1**: Integer arithmetic must be exact and unbounded in standard mode. `9007199254740993` must round-trip, and `2^53 + 1` must not collapse to `2^53`. Silently returning a different integer than the user typed is a correctness bug, not a display artifact. - **NFR-9.2**: Arithmetic on terminating decimal literals must be exact: `0.1 + 0.2` is `0.3`, and `1.1 + 2.2` is `3.3`. - **NFR-9.3**: Division must be exact where the result is rational: `1/3` retains its exact value and `(1/3) * 3` is exactly `1`. - **NFR-9.4**: Unit conversions between units whose factors are exact decimals must be exact: `12 in to ft` is exactly `1`. - **NFR-9.5**: Exactness is tracked, not assumed. A value is exact only if no rounding occurred anywhere in its computation. Any operation involving an inexact operand yields an inexact result, and inexact values never silently become exact again. - **NFR-9.6**: Operations that cannot produce a rational result (trigonometric, logarithmic, exponential, non-integer powers, roots of non-perfect-powers, and the constants `pi`/`e`/`tau`) fall back to floating point. This is a per-evaluation property: `sqrt(4)` stays exact, `sqrt(2)` does not. - **NFR-9.7**: Input base must not affect the numeric path. `0xdeadbeef + 10` is exact integer arithmetic; base is a lexical and display concern only. - **NFR-9.8**: Exact arithmetic must degrade gracefully rather than exhaust memory. Denominator size is capped, and exceeding the cap demotes the value to inexact rather than erroring. - **NFR-9.9**: Display precision is a separate decision from compute precision. Exact values with terminating decimal expansions print exactly; non-terminating ones are rendered to a digit budget with correct rounding and marked approximate, with the exact fraction available. - **NFR-9.10**: Programmer mode retains fixed-width `u128` wrapping semantics, and the IEEE 754 float view retains binary `f32`/`f64` semantics. Neither is affected by exact arithmetic. - **NFR-9.11**: The inexact tier must not claim more precision than its operations can deliver. Displayed digits must be digits we can justify, so the storage width is chosen to match the accuracy of the available transcendental implementations rather than exceeding it (see design.md 2.7.8).