# Design: Tally - Cross-Platform Calculator ## References - #[[file:.kiro/specs/calculator/requirements.md]] --- ## 1. System Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Frontends │ ├─────────────┬──────────────────┬────────────────────────────┤ │ CLI │ TUI │ Android │ │ (Zig) │ (Zig+libvaxis) │ (Kotlin/Compose) │ └──────┬──────┴────────┬─────────┴──────────────┬─────────────┘ │ │ │ │ Zig import │ Zig import │ JNI (C ABI) │ │ │ ┌──────▼───────────────▼────────────────────────▼─────────────┐ │ Engine (Zig Library) │ ├─────────────────────────────────────────────────────────────┤ │ parser │ evaluator │ programmer │ struct_layout │ │ │ │ │ │ │ tokens │ standard │ bitwise ops │ abi_profiles │ │ ast │ financial │ base conv │ layout_compute │ └─────────────────────────────────────────────────────────────┘ ``` The engine is a pure Zig library with no I/O. Frontends depend on the engine; the engine depends on nothing outside `std`. ### Build Graph ``` build.zig (workspace root) |- engine/build.zig -> produces: static lib (.a), C header, shared lib (.so/.dylib/.dll) |- cli/build.zig -> produces: statically-linked binary |- tui/build.zig -> produces: statically-linked binary (links libvaxis) |- android targets -> produces: libtally.so for arm64-v8a, x86_64 (consumed by android/ Gradle project for APK packaging) ``` **Build system philosophy:** - `zig build` is the single entry point for all compilation. No Makefile, no CMake, no shell scripts. - `zig build` (no args) -> builds engine + CLI + TUI for the host platform. - `zig build -Dtarget=aarch64-linux-android` -> cross-compiles engine .so for Android arm64. - `zig build test` -> runs all engine unit tests with coverage reporting. - The `android/` Gradle project handles only APK packaging and Compose UI compilation - it does not invoke Zig. Prebuilt .so files are committed or produced by a prior `zig build` step. **Toolchain management (mise):** - `.mise.toml` at project root declares: Zig version, any other needed tools. - `mise install` provisions the full dev environment. No other manual setup required. - CI uses the same `.mise.toml` for reproducibility. --- ## 2. Engine Design ### 2.1 Module Breakdown | Module | Responsibility | |--------|---------------| | `parser.zig` | Tokenizer + Pratt parser -> AST | | `ast.zig` | AST node definitions | | `evaluator.zig` | Walk AST, produce results | | `programmer.zig` | Integer operations, base conversion, bit manipulation | | `struct_layout.zig` | Struct DSL parser, layout computation, ABI profiles | | `units.zig` | Unit conversion tables and resolver | | `financial.zig` | CAGR, TVM, compound interest | | `types.zig` | Shared types (Value, Error, etc.) | | `engine.zig` | Public API surface (Zig-native) | | `c_api.zig` | `extern "C"` wrappers for JNI/FFI consumers | ### 2.2 Core Data Types ```zig /// Result of any calculation pub const Value = union(enum) { integer: Integer, float: f64, boolean: bool, struct_layout: StructLayoutResult, multi_base: MultiBaseResult, // programmer mode result }; pub const Integer = struct { /// Raw bits stored in u64, interpretation depends on context raw: u64, bit_width: BitWidth, signedness: Signedness, }; pub const BitWidth = enum { bits8, bits16, bits32, bits64 }; pub const Signedness = enum { signed, unsigned }; pub const MultiBaseResult = struct { value: Integer, decimal_signed: []const u8, // formatted string decimal_unsigned: []const u8, hex: []const u8, octal: []const u8, binary: []const u8, bit_pattern: [64]u1, // individual bits, index 0 = LSB }; ``` ### 2.3 AST Nodes ```zig pub const Expr = union(enum) { number_literal: NumberLiteral, unary_op: UnaryOp, binary_op: BinaryOp, function_call: FunctionCall, variable: []const u8, assignment: Assignment, }; pub const NumberLiteral = struct { value: f64, /// For programmer mode: preserve original base and integer value int_value: ?u64, base: Base, }; pub const Base = enum { decimal, hex, octal, binary }; pub const BinaryOp = struct { op: Operator, left: *Expr, right: *Expr, }; pub const Operator = enum { add, sub, mul, div, mod, pow, bit_and, bit_or, bit_xor, shift_left, shift_right_logical, shift_right_arithmetic, rotate_left, rotate_right, }; ``` ### 2.4 Parser Design **Approach: Pratt parser (top-down operator precedence)** Precedence table (low -> high): | Level | Operators | |-------|-----------| | 1 | `\|` (OR), `or` | | 2 | `xor` (XOR keyword) | | 3 | `&` (AND), `and` | | 4 | `<<`, `>>`, `>>>`, `rol`, `ror` | | 5 | `+`, `-` | | 6 | `*`, `/`, `%` | | 7 | `^`, `**` (exponentiation, always) | | 8 | Unary `-`, `~` (NOT), `not` | | 9 | Function calls, parentheses | **`^` is always exponentiation.** There is no mode-dependent operator overloading. XOR is a keyword (`xor`), consistent with the existing `rol`/`ror` keyword operators. Bitwise ops have both symbolic and keyword forms: `&`/`and`, `|`/`or`, `~`/`not`. XOR is keyword-only (`xor`) since `^` is reserved for power. Every operator means the same thing in standard and programmer modes. **No implicit multiplication**: adjacency (e.g. `2pi`) is an error; use explicit `*`. Spaces within hex/oct/bin literals are separators (e.g. `0xFF FF`). ### 2.5 Evaluation The evaluator maintains an `Environment`: ```zig pub const Environment = struct { mode: Mode, variables: std.StringHashMap(Value), history: std.ArrayList(HistoryEntry), programmer_config: ProgrammerConfig, pub const Mode = enum { standard, programmer, financial }; pub const ProgrammerConfig = struct { bit_width: BitWidth = .bits64, signedness: Signedness = .signed, display_endian: Endianness = .little, }; }; ``` Standard mode evaluates to `f64`. Programmer mode evaluates to `Integer` (exact, truncated to bit width). Financial functions return `f64` with optional step-by-step breakdown. ### 2.6 Number Display Formatting The engine provides raw values; frontends apply display formatting. The engine includes a formatting module that produces display strings and raw (clipboard-friendly) strings separately. #### Decimal Formatting | Context | Display | Clipboard | |---------|---------|-----------| | Standard result | `4,294,967,295` | `4294967295` | | Financial result | `$1,234,567.89` | `1234567.89` | | Large decimals | `4,294,967,295` (never scientific unless > 15 digits of precision) | `4294967295` | **Scientific notation threshold**: Only use scientific notation when the number exceeds what can be meaningfully displayed in decimal (> 15 significant digits, or absolute value > 10^15 or < 10^-15). Otherwise, always show full decimal with commas. This is a deliberate departure from calculators that jump to scientific notation at 10+ digits. #### Programmer Mode Formatting Values are displayed with visual grouping separators to aid readability, but clipboard copies the raw value without separators. | Base | Display (value view) | Display (memory view) | Clipboard | |------|---------------------|----------------------|-----------| | Hex | `FF FF FF FF` | `FF FF FF FF` | `0xFFFFFFFF` | | Bin | `1111 1111 1111 1111 1111 1111 1111 1111` | same | `0b11111111111111111111111111111111` | | Oct | `0o37_777_777_777` | - | `0o37777777777` | | Dec | `4,294,967,295` | - | `4294967295` | Rationale for hex grouping: - **Display uses space per byte** (`FF FF FF FF`): matches hex editors, debuggers, `xxd` output. Consistent with binary's nibble-space grouping. Universally readable. - These are two different views of the same data: the *value* (what you'd write in source code) vs. the *memory* (how it sits in RAM). For this calculator, both use the same space-separated format. Binary always groups by nibble (4 bits) with spaces - universally expected by programmers. #### Formatting Data Structure ```zig pub const FormattedValue = struct { /// Human-readable display string (with commas, underscores, spaces) display: []const u8, /// Machine/clipboard string (raw, no separators, includes prefix) raw: []const u8, }; ``` All frontends: - Show `display` in the UI - Copy `raw` to clipboard on long-press (Android) or yank (TUI) --- ## 3. Struct Layout Engine ### 3.1 Mini-DSL Grammar ``` struct_def := "struct" "{" field_list "}" field_list := (field ";")* field := type_name identifier type_name := "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" | "f32" | "f64" | "bool" | "ptr" | "[" number "]" type_name // arrays identifier := [a-zA-Z_][a-zA-Z0-9_]* ``` ### 3.2 ABI Profile Interface ```zig pub const AbiProfile = struct { name: []const u8, pointer_size: u8, // bytes max_alignment: u8, // maximum alignment enforced /// Given a field type, return its (size, alignment) in bytes field_layout: *const fn (field_type: FieldType) -> struct { size: u8, alignment: u8 }, /// Struct-level alignment (usually max of all field alignments) struct_alignment: *const fn (fields: []const FieldInfo) -> u8, /// Whether tail padding is added to round up to struct alignment tail_padding: bool, }; // Built-in profiles pub const x86_64_sysv = AbiProfile{ ... }; // default pub const x86_64_win64 = AbiProfile{ ... }; // future pub const packed = AbiProfile{ ... }; // no padding ``` ### 3.3 Layout Computation Algorithm ``` function compute_layout(fields, abi): offset = 0 struct_align = 1 result_fields = [] for each field in fields: (size, align) = abi.field_layout(field.type) struct_align = max(struct_align, align) # Insert padding for alignment padding = (align - (offset % align)) % align if padding > 0: emit_padding(offset, padding) offset += padding # Place field emit_field(field.name, field.type, offset, size) offset += size # Tail padding if abi.tail_padding: tail = (struct_align - (offset % struct_align)) % struct_align if tail > 0: emit_padding(offset, tail) offset += tail return StructLayout { total_size: offset, alignment: struct_align, fields: result_fields, } ``` ### 3.4 Layout Result Structure ```zig pub const StructLayoutResult = struct { total_size: usize, alignment: u8, fields: []const LayoutField, bytes: []const ByteInfo, // byte-by-byte map for visualization abi_name: []const u8, }; pub const LayoutField = struct { name: []const u8, type_name: []const u8, offset: usize, size: usize, alignment: u8, padding_before: usize, }; pub const ByteInfo = union(enum) { field: struct { field_index: usize, byte_within_field: usize }, padding: struct { before_field_index: usize }, tail_padding: void, }; ``` --- ## 4. Unit Conversion Engine ### 4.1 Architecture Units are organized by category. Each category has a canonical base unit. All conversions go through the base unit (from -> base -> to), except temperature which uses formula-based conversion. ```zig pub const UnitCategory = enum { length, mass, temperature, time, digital_storage, speed, area, volume, energy, pressure, data_rate, angle, }; pub const UnitDef = struct { name: []const u8, // canonical name (e.g., "km") aliases: []const []const u8, // alternatives (e.g., "kilometer", "kilometers") category: UnitCategory, /// Conversion to base unit: value_in_base = value * factor + offset /// For most units, offset = 0. Temperature uses both. to_base_factor: f64, to_base_offset: f64, from_base_factor: f64, from_base_offset: f64, }; ``` ### 4.2 Conversion Resolution The parser recognizes the pattern ` to `: - Tokenizer identifies known unit names after a numeric expression - `to` keyword triggers conversion mode - Both units must be in the same category (error otherwise) For expressions like `5 kg + 3 lb`, the parser: 1. Identifies `5 kg` as a unit-qualified value 2. Sees `+ 3 lb` - converts `3 lb` to `kg` (left-hand unit wins) 3. Evaluates as `5 + 1.36078 = 6.36078 kg` ### 4.3 Adding New Units New units require only a table entry - no parser changes: ```zig // In units/length.zig pub const length_units = [_]UnitDef{ .{ .name = "m", .aliases = &.{"meter", "meters"}, .to_base_factor = 1.0, ... }, .{ .name = "km", .aliases = &.{"kilometer", "kilometers"}, .to_base_factor = 1000.0, ... }, .{ .name = "mi", .aliases = &.{"mile", "miles"}, .to_base_factor = 1609.344, ... }, // ... add new entries here }; ``` --- ## 5. Financial Module ### 5.1 CAGR ``` CAGR = (end_value / start_value) ^ (1 / periods) - 1 ``` ### 5.2 TVM (Time Value of Money) The five TVM variables: N (periods), I/Y (interest rate per period), PV (present value), PMT (payment), FV (future value). Relationship: ``` PV * (1 + r)^N + PMT * [((1 + r)^N - 1) / r] + FV = 0 ``` where `r = I/Y / 100`. Solving for the unknown variable: - **FV, PV, PMT**: algebraic (closed-form) - **N**: logarithmic solution - **I/Y**: Newton-Raphson iterative solver (no closed form) --- ## 6. C API (for Android/FFI) ```zig // c_api.zig - extern "C" exports // // SAFETY: All string inputs use pointer + length (not null-terminated). // This avoids buffer overread vulnerabilities and allows embedded nulls. // All returned strings include a length - caller must free with tally_result_free(). pub const CalcString = extern struct { ptr: [*]const u8, len: usize, }; pub const CalcResult = extern struct { /// JSON-encoded result. Null if error. json_ptr: ?[*]u8, json_len: usize, /// Error message if json_ptr is null. error_ptr: ?[*]u8, error_len: usize, }; /// Evaluate an expression, return JSON result. /// Caller must free the result with tally_result_free(). export fn tally_eval( expr_ptr: [*]const u8, expr_len: usize, mode: c_int, // 0=standard, 1=programmer, 2=financial config_ptr: ?[*]const u8, // optional JSON config (null if none) config_len: usize, // 0 if no config ) callconv(.c) CalcResult; /// Compute struct layout, return JSON result. export fn tally_struct_layout( def_ptr: [*]const u8, def_len: usize, abi_ptr: [*]const u8, // "sysv", "win64", "packed" abi_len: usize, endian: c_int, // 0=little, 1=big ) callconv(.c) CalcResult; /// Convert a value between units, return JSON result. export fn tally_convert( value: f64, from_ptr: [*]const u8, from_len: usize, to_ptr: [*]const u8, to_len: usize, ) callconv(.c) CalcResult; /// Free a CalcResult's buffers. export fn tally_result_free(result: *CalcResult) callconv(.c) void; /// Get version string (static lifetime, do not free). export fn tally_version(out_len: *usize) callconv(.c) [*]const u8; ``` Design notes: - **No null-terminated strings.** All inputs take `(ptr, len)` pairs. This prevents buffer overread, allows binary data in expressions if ever needed, and is the idiomatic C pattern for length-known strings. - **`CalcResult` struct** bundles success/error in a single return. Caller checks `json_ptr != null` for success. - **JSON result format** keeps the Android/FFI boundary simple - Kotlin parses JSON natively. On the JNI side, the Kotlin bridge reads `(ptr, len)` into a `ByteArray` and decodes UTF-8. --- ## 7. CLI Design ``` tally [OPTIONS] tally struct [OPTIONS] tally convert tally cagr tally tvm [--n N] [--rate R] [--pv PV] [--pmt PMT] [--fv FV] Options: -p, --programmer Programmer mode -b, --bits Bit width: 8, 16, 32, 64 (default: 64) --base Output base: dec, hex, oct, bin, all (default: all) --json JSON output --abi Struct ABI: sysv (default), packed --endian Endianness: little (default), big -h, --help Show help --version Show version ``` ### CLI Output Examples Standard: ``` $ tally "2^32 - 1" 4,294,967,295 ``` Programmer: ``` $ tally -p "0xFF & 0x0F" dec(signed): 15 dec(unsigned): 15 hex: 0x0000_0000_0000_000F oct: 0o17 bin: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 bits: [64] ``` Struct: ``` $ tally struct "struct { u32 x; u8 flags; u16 id; u64 ts; }" ABI: x86-64 System V | Endian: little Offset Size Field Type Align Padding ────── ──── ────────── ───── ───── ─────── 0 4 x u32 4 4 1 flags u8 1 5 1 (padding) 1 byte 6 2 id u16 2 8 8 ts u64 8 Total: 16 bytes (alignment: 8, padding: 1 byte, tail: 0) Memory map: 00: [xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx] x 04: [ffff ffff] [········] [iiii iiii iiii iiii] flags, pad, id 08: [tttt tttt tttt tttt tttt tttt tttt tttt ts 0C: tttt tttt tttt tttt tttt tttt tttt tttt] ``` Unit conversion: ``` $ tally convert 100 km miles 62.1371 miles $ tally "72 F to C" 22.2222 °C $ tally "5 kg + 3 lb" 6.36078 kg ``` --- ## 8. TUI Layout ### 8.1 Standard Mode ``` ┌─ Tally ─────────────────────── [Standard] [Programmer] [Financial] ─┐ │ │ │ History: │ │ 2^10 = 1024 │ │ sqrt(144) = 12 │ │ Ans * 2 = 24 │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ > 3 * pi + 1_ │ │ │ │ = 10.42477796... │ │ │ └───────────────────────────── [d]ec [h]ex [o]ct [b]in ── q:quit ── ?:help┘ ``` ### 8.2 Programmer Mode ``` ┌─ Tally ─────────────────────── [Standard] [Programmer] [Financial] [Convert] ─┐ │ Bit Width: [64] Signed: [yes] Endian: [LE] │ │ │ │ ┌─ Bit Pattern (63 -> 0) ─────────────────────────────────────────────────────┐ │ │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │ │ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ cursor: bit 3 ▲ │ │ │ │ Signed: 255 │ │ Unsigned: 255 │ │ Hex: 0x0000_0000_0000_00FF │ │ Oct: 0o377 │ │ Bin: 0000 0000 0000 0000 0000 0000 1111 1111 │ │ │ │ > 0xFF & 0x0F_ │ │ = 15 │ │ │ └──── arrows:nav Space:toggle w:width s:sign e:endian y:yank -- Tab:mode ----┘ ``` **NOTE: The wireframe above (8.2) is superseded by the editable-fields design below. Keeping the old wireframe for reference during transition.** #### 8.2.1 Programmer Mode - Editable Fields (REVISED DESIGN) The programmer mode is an **editable multi-field form** where every representation of the value is live-editable. A change in any field instantly updates all others. Closer to integer.exposed than a traditional calculator with a prompt. Fields (navigable with Up/Down): 1. Bit grid - arrow keys move, Space toggles individual bits 2. DEC(s) - editable signed decimal 3. DEC(u) - editable unsigned decimal 4. HEX - editable hex (space-separated bytes: FF 00 AB CD) 5. ASCII - read-only (derived) view of the bytes, one glyph per byte, MSB first, aligned column-for-column under the HEX row directly above it. Printable bytes (0x20-0x7E) show as themselves; every other byte shows as `.`, matching the `xxd` hex-dump convention (e.g. `.ELF`, `...ascii`). Not part of the Up/Down focus cycle since it has nothing to edit. 6. OCT - editable octal 7. BIN - editable binary (nibble-separated: 1111 0000) 8. Expression - full expression evaluator (& | ^ ~ << >> etc.) Navigation: - Up/Down moves focus between fields - When a text field is focused, typing replaces its content; value updates on Enter (or live as you type - TBD) - When bit grid is focused: left/right/up/down navigate, Space toggles - Tab cycles between Standard/Programmer mode - Ctrl-W cycles bit width (8/16/32/64/128) - Ctrl-E toggles endianness display (LE/BE) Bit width is a non-destructive display lens: - The stored value keeps full precision (u128). Cycling the width never truncates it, so narrowing to a smaller width and then widening again restores the hidden upper bits. - The DEC/HEX/OCT/BIN/ASCII rows and the bit grid show the value masked to the current width. - While the stored value has bits set beyond the current width, a warning line is shown ("value exceeds N bits - showing low bits (widen to restore)"). It is a warning, not an error: the value is still usable. - Explicit value edits (typing a decimal digit, toggling a bit, editing a hex/oct/bin digit) commit to the current width, clearing any hidden upper bits. Only width cycling is non-destructive; deliberately editing at a narrow width means you are now working at that width. Endianness: - Toggle with Ctrl-E; header shows current setting (`Endian: LE` / `Endian: BE`) - Default: big-endian. The HEX row's primary job is to show the value in hex, so by default it reads MSB-first and matches DEC/OCT/BIN. Little-endian is the opt-in "how it sits in x86-64 memory" view. - Affects: HEX byte display order and ASCII byte order only. In LE the display bytes are reversed (e.g. 0xDEADBEEF -> `EF BE AD DE`). - Does NOT affect: the underlying value, DEC, OCT, or BIN display, nor the clipboard `raw` form of HEX (which stays the canonical MSB-first number). - Rationale for BE default (revised from an earlier LE default): defaulting to LE would reverse the HEX row so it no longer matches the number shown in the DEC/BIN rows directly beside it, which is surprising in a calculator context. Text/ASCII literals: - Single-quoted strings in expressions: `'ELF'`, `'RIFF'`, `'a'` - Each character becomes one byte packed into the integer - Packing order follows endianness setting: - BE: first char -> most significant used byte (`'AB'` = 0x4142) - LE: first char -> least significant byte (`'AB'` = 0x4241) - Max chars = bit_width / 8 (1 for 8-bit, 2 for 16, 4 for 32, 8 for 64) - Non-ASCII characters are an error - Useful for: magic numbers (`'ELF'`), protocol tags, inspecting how string bytes pack into integer registers IEEE 754 float interpretation: - A sub-view within programmer mode (toggle with a key, e.g. Ctrl-F) - Reinterprets the current bit pattern as an IEEE 754 float - Supported formats: - float32 (1 sign + 8 exponent + 23 significand, bias 127) - float64 (1 sign + 11 exponent + 52 significand, bias 1023) - Bit grid color-codes the three fields: - Sign bit: one color (e.g. pink) - Exponent: another color (e.g. cyan) - Significand: another color (e.g. green) - Display below the bit grid shows: - Decimal value (exact) - Formula: (-1)^s * 2^(e-bias) * 1.mantissa - Classification: normal, denormal, zero, infinity, NaN - ULP: delta to next representable value - Raw fields: sign=0, exp=127 (unbiased: 0), frac=0x000000 - Input: can type a decimal float value (e.g. `3.14`) and it shows the closest representable bit pattern, noting if rounding occurred - Auto-entry: switching into programmer mode (Tab) with a non-integer value in `ans` opens this float view automatically on the exact f64 bit pattern, since the integer bit views cannot represent a fraction. An integer `ans` uses the integer views as usual. Infinity/NaN also open the float view (the only place they are representable). - Toggling bits in the grid updates the float interpretation live - Does NOT change bit_width setting - float32 uses 32-bit view, float64 uses 64-bit view (auto-switches width when toggling float format) ### 8.3 Struct Visualizer (sub-view of Programmer Mode) ``` ┌─ Tally ────────────────── [Programmer > Struct Layout] ─────────────┐ │ ABI: x86-64 SysV Endian: LE │ │ │ │ Definition: │ │ struct { u32 x; u8 flags; u16 id; u64 ts; } │ │ │ │ ┌─ Memory Map ─────────────────────────────────────────────────────┐ │ │ │ 00 01 02 03 │ 04 │ 05 │ 06 07 │ 08 09 0A 0B 0C 0D 0E 0F │ │ │ │ │ ████████████ │ ▓▓ │ ░░ │ ▒▒▒▒▒ │ ░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ │ x │flag│pad │ id │ ts │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ Field Type Offset Size Align Pad Before │ │ x u32 0 4 4 0 │ │ flags u8 4 1 1 0 │ │ id u16 6 2 2 1 │ │ ts u64 8 8 8 0 │ │ │ │ Total: 16 bytes | Alignment: 8 | Padding: 1 byte (6.25%) │ │ Packed would be: 15 bytes │ └──── a:abi e:endian Enter:edit struct Esc:back ─────────────────────┘ ``` --- ## 9. Android UI Design The Android app uses a fundamentally different interaction model from the TUI. Where the TUI is keyboard-driven with a prompt, Android is **touch-first with purpose-built input surfaces** for each mode. No text-cursor-in-a-prompt paradigm - instead, tappable buttons, interactive grids, and form fields. ### 9.1 Navigation & Shell Bottom navigation bar with 4 destinations: **Standard**, **Programmer**, **Financial**, **Convert**. ``` ┌─────────────────────────────────────────┐ │ ┌─────────────────────────────────┐ │ │ │ │ │ │ │ Mode Content Area │ │ │ │ │ │ │ └─────────────────────────────────┘ │ │ │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 🔢 Standard 💻 Programmer 💰 Finance 🔄 Convert │ └─────────────────────────────────────────┘ ``` ### 9.2 Standard Calculator Screen Classic calculator layout. Expression builds at top, result previews live below it, button grid fills the bottom half. ``` ┌─────────────────────────────────────────┐ │ Expression area (scrollable) │ │ ┌─────────────────────────────────┐ │ │ │ 2 × (3 + 4) │ │ ← expression in natural format │ │ = 14 │ │ ← live result preview (dimmed) │ └─────────────────────────────────┘ │ │ │ │ History peek: last 2-3 results │ │ ┌─────────────────────────────────┐ │ │ │ 5^2 = 25 sqrt(144) = 12│ │ │ └─────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ sin │ cos │ tan │ ( │ ) │ ⌫ │ │ ← function row (swipeable for more) │ ├─────┼─────┼─────┼─────┼─────┼───┤ │ │ │ 7 │ 8 │ 9 │ ÷ │ ^ │ │ │ │ ├─────┼─────┼─────┼─────┼─────┤ │ │ │ │ 4 │ 5 │ 6 │ × │ % │ │ │ │ ├─────┼─────┼─────┼─────┼─────┤ │ │ │ │ 1 │ 2 │ 3 │ - │ sqrt│ │ │ │ ├─────┼─────┼─────┼─────┼─────┤ │ │ │ │ 0 │ . │ Ans │ + │ = │ │ │ │ └─────┴─────┴─────┴─────┴─────┘ │ │ └─────────────────────────────────────────┘ ``` **Key interactions:** - Tap buttons to build expression (no keyboard typing required) - Long-press `=` to copy result - Swipe function row for more functions (log, ln, abs, etc.) - Tap history items to recall into expression - Pull-down to expand full history view ### 9.3 Programmer Mode Screen Split into three vertical zones: **value display**, **bit grid**, and **input buttons**. ``` ┌─────────────────────────────────────────┐ │ ┌─── Bit Width ────────────────────┐ │ │ │ [ 8 ] [ 16 ] [ 32 ] [●64] │ │ ← segmented control │ └──────────────────────────────────┘ │ │ │ │ ┌─── Value Display ────────────────┐ │ │ │ DEC 4,294,967,295 │ │ ← tappable to switch primary │ │ HEX 0xFFFF_FFFF │ │ │ │ OCT 0o37_777_777_777 │ │ │ │ BIN 1111 1111 1111 1111 11.. │ │ │ │ SIGN -1 (signed) │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌─── Bit Grid ─────────────────────┐ │ │ │ 31 30 29 28 │ 27 26 25 24 │ │ ← bit index labels │ │ 1 1 1 1 │ 1 1 1 1 │ │ ← tappable cells │ │ 23 22 21 20 │ 19 18 17 16 │ │ │ │ 1 1 1 1 │ 1 1 1 1 │ │ │ │ 15 14 13 12 │ 11 10 9 8 │ │ │ │ 1 1 1 1 │ 1 1 1 1 │ │ │ │ 7 6 5 4 │ 3 2 1 0 │ │ │ │ 1 1 1 1 │ 1 1 1 1 │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌─── Operators ────────────────────┐ │ │ │ AND│ OR │XOR │NOT │ << │ >> │ROL │ │ ← bitwise op buttons │ ├────┼────┼────┼────┼────┼────┼────┤ │ │ │ A │ B │ C │ D │ E │ F │ │ │ ← hex input │ ├────┼────┼────┼────┼────┼────┤ │ │ │ │ 7 │ 8 │ 9 │ 0x│ 0b│CLR │ │ │ ← digits + base prefix │ ├────┼────┼────┼────┼────┼────┤ │ │ │ │ 4 │ 5 │ 6 │ + │ - │ = │ │ │ │ ├────┼────┼────┼────┼────┼────┤ │ │ │ │ 1 │ 2 │ 3 │ × │ ÷ │ ⌫ │ │ │ │ └────┴────┴────┴────┴────┴────┘ │ │ └─────────────────────────────────────────┘ ``` **Key interactions:** - Tap any bit in the grid to toggle it - all displays update instantly - Haptic pulse on bit toggle - Tap DEC/HEX/OCT/BIN label to make that the primary input base - Bit grid scrolls horizontally for 64-bit (or collapses to 4 rows of 16) - FAB or toolbar button for "Struct Layout" sub-screen ### 9.4 Struct Layout Screen (Programmer sub-screen) Accessed via a button within Programmer mode. Uses a **form-based struct builder** rather than raw text DSL (though raw text input is available via a toggle). ``` ┌─────────────────────────────────────────┐ │ ← Back to Programmer │ │ │ │ ┌─── ABI / Settings ──────────────┐ │ │ │ ABI: [x86-64 SysV ▾] Endian: [LE ▾]│ │ └──────────────────────────────────┘ │ │ │ │ ┌─── Fields ───────────────────────┐ │ │ │ ┌────────┬──────────┬─────────┐ │ │ │ │ │ Type ▾ │ Name │ ⊖ │ │ │ ← each row is a field │ │ ├────────┼──────────┼─────────┤ │ │ │ │ │ u32 │ x │ ⊖ │ │ │ │ │ │ u8 │ flags │ ⊖ │ │ │ │ │ │ u16 │ id │ ⊖ │ │ │ │ │ │ u64 │ ts │ ⊖ │ │ │ │ │ └────────┴──────────┴─────────┘ │ │ │ │ [ + Add Field ] │ │ │ │ [ ✏️ Raw DSL input toggle ] │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌─── Memory Map ───────────────────┐ │ │ │ ██████████ ▓▓ ░░ ▒▒▒▒ ░░░░░░░░░ │ │ ← color-coded byte bar │ │ x(4) fl p id ts(8) │ │ ← labels below │ └──────────────────────────────────┘ │ │ │ │ ┌─── Summary ──────────────────────┐ │ │ │ Total: 16 bytes Align: 8 │ │ │ │ Padding: 1 byte (6.25%) │ │ │ │ Packed: 15 bytes │ │ │ └──────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` **Key interactions:** - Type dropdown for each field (Material exposed dropdown menu) - Swipe-to-delete or tap ⊖ to remove a field - Drag handles to reorder fields (see padding change in real-time) - Memory map is a custom Canvas composable with tappable segments (tap a segment -> highlights field info) - Toggle for raw DSL text input (for copy-paste from code) - Share button: export struct layout as text or image ### 9.5 Financial Mode Screen Card-based interface with function selector at top. ``` ┌─────────────────────────────────────────┐ │ ┌─── Function ────────────────────┐ │ │ │ [●CAGR] [TVM] [Compound] [PV] │ │ ← scrollable chip row │ └─────────────────────────────────┘ │ │ │ │ ┌─── CAGR Inputs ────────────────┐ │ │ │ │ │ │ │ Start Value │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ 10,000 │ │ │ ← text field │ │ └─────────────────────────┘ │ │ │ │ │ │ │ │ End Value │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ 25,000 │ │ │ │ │ └─────────────────────────┘ │ │ │ │ │ │ │ │ Periods (years) │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ 5 │ │ │ │ │ └─────────────────────────┘ │ │ │ │ │ │ │ │ [ Calculate ] │ │ │ └─────────────────────────────────┘ │ │ │ │ ┌─── Result ─────────────────────┐ │ │ │ CAGR = 20.11% │ │ │ │ │ │ │ │ Formula: │ │ │ │ (25000/10000)^(1/5) - 1 │ │ │ │ = 2.5^0.2 - 1 │ │ │ │ = 0.2011... │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` **Key interactions:** - Chip row selects function type, input form adapts - For TVM: 5 input fields, user leaves one blank -> that's the solve target - Compute triggers engine call, result animates in - Result card shows formula breakdown (expandable) - Long-press result to copy ### 9.6 Convert Mode Screen Designed for fast, one-handed unit conversion. ``` ┌─────────────────────────────────────────┐ │ ┌─── Category ────────────────────┐ │ │ │ [Length] [Mass] [Temp] [Time] ▸ │ │ ← scrollable chip row │ └─────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ │ │ │ │ 100 │ │ ← large editable number │ │ kilometers [▾] │ │ ← unit selector dropdown │ │ │ │ │ │ ⇅ (swap) │ │ ← tap to swap from/to │ │ │ │ │ │ 62.1371 │ │ ← live result (updates as you type) │ │ miles [▾] │ │ ← unit selector dropdown │ │ │ │ │ └─────────────────────────────────┘ │ │ │ │ ┌─── Keypad ─────────────────────┐ │ │ │ 7 │ 8 │ 9 │ │ │ │ ├─────┼─────┼─────┤ ⌫ │ │ │ │ 4 │ 5 │ 6 │ │ │ │ ├─────┼─────┼─────┼─────────────┤ │ │ │ 1 │ 2 │ 3 │ │ │ │ ├─────┼─────┼─────┤ C │ │ │ │ 0 │ . │ ± │ │ │ │ └─────┴─────┴─────┴─────────────┘ │ └─────────────────────────────────────────┘ ``` **Key interactions:** - Conversion updates live as user types (no "calculate" button needed) - Swap button rotates from/to units with an animation - Unit selectors open a searchable bottom sheet (for categories with many units) - Category chips filter which units appear in the selectors - Recent conversions remembered ### 9.7 Tablet Layout (landscape / large screen) On tablets, modes use a two-pane layout: ``` ┌─────────────────────┬──────────────────────────────┐ │ │ │ │ Input / Controls │ Results / Visualization │ │ │ │ │ (button grid, │ (history, bit grid, │ │ form fields, │ memory map, formula │ │ struct editor) │ breakdown) │ │ │ │ └─────────────────────┴──────────────────────────────┘ │ 🔢 Standard 💻 Programmer 💰 Finance 🔄 Convert │ └─────────────────────────────────────────────────────┘ ``` ### 9.8 Android Architecture (Technical) ``` ┌──────────────────────────────────────┐ │ Compose UI Layer │ │ ├── StandardScreen │ │ ├── ProgrammerScreen │ │ │ ├── BitGridComposable │ │ │ └── StructVisualizerScreen │ │ ├── FinancialScreen │ │ └── ConvertScreen │ ├──────────────────────────────────────┤ │ ViewModel Layer │ │ ├── CalculatorViewModel │ │ ├── ProgrammerViewModel │ │ ├── FinancialViewModel │ │ ├── ConvertViewModel │ │ └── manages state, calls engine │ ├──────────────────────────────────────┤ │ JNI Bridge (TallyEngine.kt) │ │ ├── external fun evaluate(...) │ │ ├── external fun structLayout(...) │ │ ├── external fun convert(...) │ │ └── JSON serialization/deser │ ├──────────────────────────────────────┤ │ Native .so (libtally.so) │ │ └── C ABI from engine/c_api.zig │ └──────────────────────────────────────┘ ``` The JNI bridge sends expression strings down and receives JSON results back. This keeps the boundary thin and debuggable. --- ## 10. Error Handling Strategy ```zig pub const CalcError = error{ // Parser errors UnexpectedToken, UnmatchedParen, InvalidNumber, UnknownFunction, UnknownVariable, // Evaluation errors DivisionByZero, Overflow, InvalidOperandType, DomainError, // e.g., sqrt(-1) // Struct layout errors InvalidType, InvalidFieldName, DuplicateFieldName, StructTooLarge, // Financial errors InsufficientParameters, ConvergenceFailure, // TVM Newton-Raphson didn't converge // System OutOfMemory, }; pub const ErrorInfo = struct { err: CalcError, message: []const u8, position: ?usize, // character position in input where error occurred context: []const u8, // snippet of input around error }; ``` All engine functions return `CalcError!Result`. Frontends translate these into user-facing messages with position highlighting. --- ## 11. Key Design Decisions | # | Decision | Rationale | |---|----------|-----------| | 1 | Pratt parser over recursive descent | Easier to extend precedence levels; cleaner for infix with mixed prefix operators | | 2 | `u64` storage for all programmer integers | Simplest representation; bit_width used for masking/interpretation, not storage | | 3 | JSON over C ABI boundary | Avoids complex struct marshaling; Kotlin/Swift handle JSON natively; small perf cost acceptable for calculator | | 4 | Separate parser for struct DSL | Struct grammar is different enough from expressions; keeps both parsers simple | | 5 | ABI as interface/vtable | Adding Windows x64 or ARM is implementing one struct; no changes to layout algorithm | | 6 | libvaxis for TUI | Most mature Zig TUI; supports Windows/Mac/Linux; active development; used by Ghostty | | 7 | `^` always means power; XOR is the `xor` keyword | Avoids mode-dependent operator overloading; every operator means the same thing in both modes; consistent with existing `rol`/`ror` keyword operators | | 8 | Statically-linked desktop binaries | Zero dependencies for end user; Zig makes this trivial | | 9 | Unit conversion via base-unit normalization | Simple to implement, easy to extend; only temperature needs special case (offset) | | 10 | Android: button grids, not text prompts | Touch-first UX; no keyboard needed for basic use; purpose-built surfaces per mode beat a generic expression prompt | | 11 | Android: form-based struct builder + raw toggle | Most users won't want to type DSL syntax on a phone; visual builder is more natural; power users can toggle to raw text | | 12 | Android: live conversion without "Calculate" button | Conversions are instant; eliminating a tap makes the UX feel responsive and direct |