# Tasks: Tally - Cross-Platform Calculator ## References - #[[file:.kiro/specs/calculator/requirements.md]] - #[[file:.kiro/specs/calculator/design.md]] --- ## Phase 1: Project Scaffolding & Engine Foundation ### Task 1.1: Initialize workspace build structure [DONE] - Create `.mise.toml` declaring Zig version (pin to specific release) - Create top-level `build.zig` with module declarations for engine, cli, tui - Add Android cross-compilation targets to `build.zig` (`-Dtarget=aarch64-linux-android`, `-Dtarget=x86_64-linux-android`) - Create `engine/build.zig` producing static lib and shared lib targets - Create `cli/build.zig` linking engine - Create `tui/build.zig` linking engine + libvaxis dependency - Add `.gitignore`, `README.md` (minimal), `LICENSE` - Verify: `mise install` provisions Zig, `zig build` compiles without errors on empty main files NOTE: Actual structure diverged from spec - single binary at `src/main.zig` (CLI + TUI combined), engine as static lib + shared lib. No separate cli/ or tui/ build files. kcov-based coverage wired in via `build/Coverage.zig`. ### Task 1.2: Implement core types module [DONE] - Create `engine/src/types.zig` - Define `Value` union, `Integer` struct, `BitWidth`, `Signedness`, `Endianness` enums - Define `MultiBaseResult` struct - Define `CalcError` error set and `ErrorInfo` struct - Define `Mode` enum (standard, programmer, financial) - Verify: compiles, types are importable from other engine modules ### Task 1.3: Implement tokenizer [DONE] - Create `engine/src/tokenizer.zig` - Token types: numbers (dec, hex `0x`, oct `0o`, bin `0b`), operators, parens, identifiers, comma, semicolon, EOF - Handle implicit multiplication detection (emit synthetic `*` token) - Handle multi-character operators: `**`, `<<`, `>>`, `>>>`, `rol`, `ror` - Support `_` and `,` as digit separators in number literals (e.g., `1_000_000`, `1,000,000`) - Verify: unit tests for tokenizing expressions in all bases, edge cases (negative numbers, adjacent tokens for implicit mult) NOTE: Implicit multiplication handled in parser rather than tokenizer. Comma-as-separator only consumed when followed by a digit (avoids eating function arg commas). ### Task 1.4: Implement Pratt parser [DONE] - Create `engine/src/ast.zig` with AST node definitions - Create `engine/src/parser.zig` - Implement Pratt parser with precedence table from design doc - Accept `Mode` parameter to resolve `^` ambiguity (power vs XOR) - Handle function calls: `name(arg1, arg2, ...)` - Handle variable assignment: `X = expr` - Handle implicit multiplication - Verify: unit tests parsing complex expressions, operator precedence, all bases, function calls, mode-dependent `^` ### Task 1.5: Implement standard mode evaluator [DONE] - Create `engine/src/evaluator.zig` - Environment struct with variables, history, config (in evaluator.zig, not separate file) - Evaluate AST to `f64` for standard mode - Implement built-in functions: sin, cos, tan, asin, acos, atan, log, ln, sqrt, cbrt, abs, ceil, floor, round, factorial - Implement constants: pi, e, tau - Implement variable storage and `Ans` - Implement history recording - Verify: unit tests for arithmetic, precedence, functions, variables, history recall ### Task 1.6: Implement programmer mode evaluator [DONE] - Create `engine/src/programmer.zig` - All operations on `Integer` (u64 storage, masked to bit_width) - Implement bitwise operators: AND, OR, XOR, NOT, shifts, rotates - Implement base conversion output (produce `MultiBaseResult`) - Implement bit_pattern array generation - Handle overflow/truncation per configured bit_width - Signed vs unsigned interpretation - Verify: unit tests for bitwise ops at all widths, base conversions, overflow behavior, sign extension ### Task 1.7: Implement number display formatter [DONE] - Create `engine/src/formatter.zig` - Define `FormattedValue` struct with `display` (human-readable) and `raw` (clipboard) strings - Decimal formatting: comma-separated groups of 3 (`4,294,967,295`) - Scientific notation only when value > 10^15 or < 10^-15 or > 15 significant digits - Hex formatting: underscore per 16-bit word for value view (`0xFFFF_FFFF`), space per byte for memory view (`FF FF FF FF`) - Binary formatting: space per nibble (`1111 1111 1111 1111`) - Octal formatting: underscore per 3-digit group (`0o37_777_777_777`) - All formatters produce both `display` and `raw` variants - Respect bit width (don't show leading zeros beyond the configured width in short-form contexts) - Verify: unit tests for all bases at all bit widths, edge cases (0, max values, negative signed values), scientific notation threshold --- ## Phase 2: Struct Layout & Financial Engines ### Task 2.0: Exact numeric model (Rational + Number union) [NOT STARTED] Design: see design.md 2.7. Requirements: NFR-9. Motivation (all reproducible today): `0.1 + 0.2` = `0.30000000000000004`, `9007199254740993` silently becomes `9.007199254740992e15`, `2^53 + 1` collapses, `12 in in ft` = `0.9999999999999998`, `factorial(171)` reports "unknown function". Sequenced as four reviewable commits so structural churn never mixes with behavioral change (rationale in design.md 2.7.9): #### 2.0a: Add the types, wired to nothing [DONE] - `engine/src/rational.zig`: `Rational` over `std.math.big.int.Managed`, always reduced, `q > 0`. add, sub, mul, div, negate, abs, `powInt` (negative exponents take the exact reciprocal), `sqrtExact` (null for non-perfect-squares so the caller can fall back), order/eql, `parseDecimal` (exact: `0.1` -> `1/10`), `isTerminating`, `denBitCount`, `toFloat`, `toFractionString`, `toDecimalString`. 55 tests. - `engine/src/number.zig`: `Number = union(enum) { exact: Rational, inexact: f64 }` with the contagion rule and the `max_denominator_bits = 4096` demotion cap. 30 tests. - Exposed via `engine.zig` (two lines) so the tests run under `zig build test`. Nothing else is wired: the evaluator is untouched. - Notable implementation points: - `toFloat` scales the numerator to ~72 significant bits before dividing, so the result is a single correct rounding rather than a quotient of two separately-rounded floats (which would lose precision twice and overflow for large values). - `toDecimalString` does long division, prints terminating expansions exactly, and rounds half-up at the digit budget while reporting `exact = false`. - `big.int`'s `sqrt` truncates, so `sqrtExact` squares the root back and compares to decide exactness. - Test gotcha worth remembering: Zig folds float literals at comptime as `comptime_float`, so `0.1 + 0.2 != 0.3` is FALSE at comptime. Comparisons meant to demonstrate f64 error must use runtime-known values (`var x: f64 = 0.1; _ = &x;`). - Verify: 487 tests pass (was 402), zig fmt and zlint clean, existing tests untouched. #### 2.0b: Evaluator internals become exact, `evalString` still returns f64 - Exact/inexact boundary per design.md 2.7.4. Per-evaluation, not per-function: `sqrt(4)` exact, `sqrt(2)` inexact. Deliberately not a CAS. - `NumberValue` and the AST number node GAIN an exact field rather than replacing `float`/`int_value`, so tokenizer and parser tests keep compiling. - ALL existing tests must still pass unchanged. - Payoff is visible through the f64 boundary because a single final rounding avoids today's accumulated error: `0.1 + 0.2` -> `0.3`, `1.1 + 2.2` -> `3.3`, `0.1 * 3` -> `0.3`, `12 in in ft` -> `1`. - Verify: existing suite green, plus new tests for the four fixes above. #### 2.0c: `evalString` and the display path move to `Number` - Mechanical signature churn through evaluator/formatter/CLI/TUI tests. This is the commit where `main.zig`'s output assertions get reviewed. - Lift the limits this removes: `formatter.is_integer`'s `< 2^53` gate and `evaluator.factorial`'s `x > 170` rejection (also the source of the misleading "unknown function" error). - Rewrite `tokenizer` test "parseNumber huge decimal falls back to float", whose premise becomes false once big integers are exact. - Verify: existing suite green after signature updates, no expected-value changes beyond the three items above. #### 2.0d: Exactness tests only the `Number` API can express - `9007199254740993` round-trips (currently unguarded, and the one bug the f64 boundary cannot fix), `2^53 + 1`, `1/3` retained exactly, `(1/3) * 3` = 1, unbounded `factorial`, contagion, demotion at the denominator cap. Out of scope: arbitrary-precision transcendentals (the inexact tier is `f64` because that is the precision we can justify; see design.md 2.7.8 for why f128 is NOT the upgrade path), and any change to programmer mode's `u128` semantics or the IEEE 754 float view. ### Task 2.1: Implement struct DSL tokenizer and parser - Create `engine/src/struct_layout.zig` - Tokenize: `struct`, `{`, `}`, `;`, type names, identifiers, `[`, `]`, numbers (for arrays) - Parse into a list of `FieldDef` structs: `{ name, type, array_count? }` - Support types: u8, u16, u32, u64, i8, i16, i32, i64, f32, f64, bool, ptr - Verify: unit tests parsing valid structs, error messages for malformed input ### Task 2.2: Implement ABI profile interface and x86-64 System V profile - Define `AbiProfile` struct with function pointers/vtable (field_layout, struct_alignment, tail_padding, pointer_size) - Implement `x86_64_sysv` profile: - u8/i8/bool -> size 1, align 1 - u16/i16 -> size 2, align 2 - u32/i32/f32 -> size 4, align 4 - u64/i64/f64/ptr -> size 8, align 8 - Struct alignment = max of field alignments - Tail padding to struct alignment boundary - Implement `packed` profile (all alignment = 1, no padding) - Verify: unit tests matching `pahole` output for known structs ### Task 2.3: Implement layout computation - Implement `compute_layout(fields, abi) -> StructLayoutResult` - Generate per-field: offset, size, alignment, padding_before - Generate byte-level map (`[]ByteInfo`) for visualization - Compute totals: size, alignment, total padding bytes, padding percentage - Compute "packed comparison" size - Verify: unit tests with structs of varying complexity, compare against known C compiler layouts ### Task 2.4: Implement endianness display - Given a `StructLayoutResult` and a set of field values (optional), show byte ordering - Little-endian (default): LSB at lowest address - Big-endian: MSB at lowest address - This affects the byte-map visualization labels, not the layout computation itself - Verify: unit test showing a u32 value's bytes in both endiannesses ### Task 2.5: Implement financial module - Create `engine/src/financial.zig` - Implement CAGR: `(end/start)^(1/n) - 1` - Implement compound interest (future value): `PV * (1 + r/m)^(m*t)` - Implement present value: `FV / (1 + r/m)^(m*t)` - Implement TVM solver: - Closed-form for FV, PV, PMT, N - Newton-Raphson for I/Y (rate) with convergence check, max 1000 iterations, tolerance 1e-10 - Return step-by-step formula string alongside numeric result - Verify: unit tests against known financial calculation results (textbook examples) ### Task 2.6: Implement unit conversion engine [DONE] - Created `engine/src/units.zig` - `UnitCategory` enum (12 categories) with `label()` and `baseUnit()` - `UnitDef` struct (name, aliases, category, to_base_factor, to_base_offset) with `toBase()`, `fromBase()`, `isLinear()` - Conversion tables for all 12 categories: length, mass, temperature, time, digital storage, speed, area, volume, energy, pressure, data rate, angle - `convert(value, from_name, to_name) -> ConvertResult` and `convertUnits(value, from, to) -> f64` - Temperature is the only affine category (F/K/R need offsets); base is Celsius - `findUnit`: exact case-sensitive match first, case-insensitive fallback, so `K`/`B`/`kB` keep their meaning while `KM` and `Celsius` still resolve - `parseRequest`: recognizes free-form ` to ` text so conversion is plain expression input, not a special command (see Task 4.4). Returns null with no `to` keyword so callers fall through to normal evaluation. - DESIGN CHANGE from the original spec: only the to-base mapping is stored and the inverse is derived in `fromBase()`, instead of storing a second factor/offset pair. One source of truth per unit means a table entry cannot disagree with its own reverse (see design.md 4.1) - 100 unit tests: known-answer conversions per category, temperature offsets and the -40 crossover, alias resolution, error paths, request parsing (glued values, expression values, longest-suffix and token-boundary rules), plus invariant tests (every unit round-trips, every within-category pair round-trips, base units are identity, names unique, no zero factors) - Verify: `zig build test` passes; CLI spot-checks match reference values (5280 ft/mi, 1024 MiB/GiB, 14.6959 psi/atm, 1 c = 1079252848.8 km/h) --- ## Phase 3: Engine Public API & C ABI ### Task 3.1: Implement engine public API - Create `engine/src/engine.zig` - `pub fn evaluate(env: *Environment, input: []const u8) !Value` - `pub fn computeStructLayout(definition: []const u8, abi: AbiProfile, endian: Endianness) !StructLayoutResult` - `pub fn computeCagr(start: f64, end: f64, periods: f64) !f64` - `pub fn solveTvm(params: TvmParams) !TvmResult` - `pub fn convert(value: f64, from_unit: []const u8, to_unit: []const u8) !ConvertResult` - `pub fn formatMultiBase(integer: Integer) MultiBaseResult` - Verify: integration tests exercising the full pipeline (string in -> result out) ### Task 3.2: Implement C API - Create `engine/src/c_api.zig` - Define `CalcString` (ptr + len) and `CalcResult` (json ptr/len + error ptr/len) extern structs - `export fn tally_eval(expr_ptr, expr_len, mode, config_ptr, config_len) CalcResult` - `export fn tally_struct_layout(def_ptr, def_len, abi_ptr, abi_len, endian) CalcResult` - `export fn tally_convert(value, from_ptr, from_len, to_ptr, to_len) CalcResult` - `export fn tally_result_free(result) void` - `export fn tally_version(out_len) [*]const u8` - **No null-terminated strings** - all inputs are (ptr, len) pairs - JSON serialization for all result types - Input length validation (reject > max expression length) - Verify: build shared library, write a C test program that calls the API with known inputs and validates JSON output ### Task 3.3: Cross-compilation verification - Verify `zig build` produces binaries for: - `x86_64-linux-gnu` - `aarch64-linux-gnu` - `x86_64-macos` - `aarch64-macos` - `x86_64-windows-gnu` - Verify `zig build` produces .so for Android targets: - `aarch64-linux-android` - `x86_64-linux-android` - All targets invocable from the same `build.zig` via `-Dtarget=` - Verify mise-provisioned Zig handles all targets without additional toolchain installs - Document any target-specific issues or workarounds needed --- ## Phase 4: CLI Frontend ### Task 4.1: Implement CLI argument parsing [DONE] - Single binary at `src/main.zig` (not separate cli/) - Parse args: positional expression (multi-arg joined with spaces), `-p`/`--programmer`, `--help`, `--version` - Subcommand `convert` IMPLEMENTED: `tally convert [to] `, including the glued form `tally convert 100km to mi` (numeric prefix split from the unit suffix, with `e`/`E` only treated as an exponent when followed by a digit or sign so `5eV` parses as 5 eV) - Subcommands (`struct`, `cagr`, `tvm`) NOT YET IMPLEMENTED - Verify: unit tests for arg parsing; `--help` prints usage NOTE: Multi-arg joining implemented (`tally 2 + 2` works same as `tally "2+2"`). Remaining subcommands deferred until their engine modules exist. ### Task 4.2: Implement CLI output formatting [DONE] - Standard mode: print result with comma formatting - Programmer mode: print multi-base table (dec signed, dec unsigned, hex, bin) - Convert: `formatConversion` prints `100 km = 62.13711922373339 mi`, resolving aliases to canonical names - Struct mode: NOT YET IMPLEMENTED - Financial mode: NOT YET IMPLEMENTED - `--json` flag: NOT YET IMPLEMENTED - Verify: unit tests for evaluate and formatConversion output ### Task 4.3: CLI integration and error handling [DONE] - Wire arg parser -> engine calls -> output formatter - Map `CalcError` to user-friendly stderr messages - Set exit codes: 0 success, 1 error - Verify: unit tests for error paths ### Task 4.4: Bare-expression unit conversion [DONE] - `tally 32F in C` and `tally 100 km to mi` work without any subcommand; the `convert` subcommand remains as an explicit alias for scripting - Both `to` and `in` act as separators - Implemented in the engine (`units.parseRequest`) rather than the CLI, so the TUI standard mode gets the same behavior: typing `100 km to mi` at the prompt converts and records it in history - Value may be a full expression (`2*3 kg to lb`), evaluated by the normal evaluator before conversion - `in` doubles as the name for inches, so candidate separators are tried last-to-first and the first valid reading wins: `5 in in cm`, `100 mm in in`, `1 in in in`, and `60 min in h` all resolve correctly - Detection is conservative: only a standalone whitespace-delimited separator triggers it, unit suffixes must start at a token boundary, and no separator means normal evaluation. Verified no regression on `2 + 3 * 4`, `sqrt(144)`, `e`, `min(3, 7)`, `sin(0)`, `0o777 - 0x0f` - 30 request-parsing tests plus 7 CLI-level tests - Verify: bare conversions, expression values, inch ambiguity, error paths, and unchanged behavior for ordinary expressions all confirmed by running the binary --- ## Phase 5: TUI Frontend ### Task 5.1: Set up TUI skeleton [DONE] - Single binary at `src/main.zig` + `src/tui.zig` (not separate tui/) - libvaxis vxfw.App with custom Widget (eventHandler + drawFn) - No args launches TUI; with args runs CLI - Molokai color theme - Verify: TUI launches, responds to Ctrl-C (quit) ### Task 5.2: Implement standard mode TUI view [DONE] - Expression input via vxfw.TextField (gap buffer) - Result display in scrollable history panel - History navigation with up/down arrows (saves/restores current input) - Ctrl-L clears history - ? shows help overlay (keybindings, functions, constants, operators) - Variables persist across expressions (assignment + Ans) - Verify: can type expressions, see results, browse history ### Task 5.2.1: Inline multi-base result in standard mode [DONE] - Engine: added `evalStringInfo` returning value + `has_nondecimal_literal` flag (walks AST via `hasNonDecimalLiteral` to detect any hex/oct/bin number literal) - Added `BitWidth.smallestFor(value)` helper - CLI: when flag is true and result is a non-negative integer, shows hex/oct/bin below decimal - TUI: history entries carry optional `details` lines; drawHistory reworked to render variable-height entries (flat line list, newest anchored at bottom) - Uses smallest standard bit width that holds the value - Does NOT change evaluation (still f64, `^` still power) - display only - Fractional/negative results show decimal only - Verify: `0o777 - 0x0f` shows 496 plus hex/oct/bin; `3.14` decimal only; `2 + 2` decimal only; `0xFF / 2` = 127.5 decimal only ### Task 5.3: Implement programmer mode TUI view - expression & base display [PARTIAL] - Programmer mode accessible via Tab key - Multi-base result display (dec signed, unsigned, hex, oct, bin) - Bit grid with navigation (arrows) and toggling (Space) - Width toggle (Ctrl-W cycles 8->16->32->64) - Ctrl-B toggles focus between input and bit grid - Expression evaluation through programmer engine - DONE: basic passive display + expression input + bit grid toggling - NOT DONE: editable fields (see Task 5.3.1) ### Task 5.3.1: Programmer mode editable fields rework [DONE] - Replaced `bit_grid_active: bool` with `prog_field: ProgField` enum tracking focused field - Up/Down arrows navigate between fields (bits, dec_signed, dec_unsigned, hex, oct, bin, expression) - When a text field is focused, the input line shows the field label as prompt (e.g. "HEX> ") - Enter in a non-expression field parses input as that base and updates prog_value - Bit grid: left/right navigates bits, space toggles, up/down exits to adjacent field when at grid edge - Focused field is visually highlighted (label gets inverse color) - Removed Ctrl-B (field navigation replaces it) - Expression field works as before (full programmer-mode expression evaluation) - ASCII row under HEX (read-only, xxd-style, aligned column-for-column) - Endianness toggle (Ctrl-E): flips HEX and ASCII byte order, header shows LE/BE. Default big-endian so HEX reads as the number (matching DEC/OCT/BIN); LE is the opt-in x86-64 memory-layout view. DEC/OCT/BIN and the hex `raw`/clipboard form are unaffected. Covered by formatter unit tests (BE and LE for hex and ascii). - Verify: can arrow between fields, type hex value and see all fields update, toggle bits ### Task 5.3.2: Implement text/ASCII literal parsing [DONE] - Added single-quote string token to the tokenizer (`'hello'`) - Parser recognizes string literals as a new AST node type - Programmer evaluator packs ASCII bytes into u128 value (BE packing: first char -> MSB) - Standard evaluator also supports it (returns f64) - Enforces max length = bit_width / 8 (Overflow error if too many chars) - Rejects non-ASCII characters (> 0x7F) - 128-bit width also added (u64 -> u128 throughout engine) - Verify: `'A'` = 0x41, `'ELF'` = 0x454C46, overflow on too-long strings ### Task 5.3.3: Implement IEEE 754 float interpretation [DONE] - Engine module `engine/src/float_interp.zig`: - `FloatFormat` (f32/f64) with total/exponent/significand bit counts + bias - `decompose(format, bits)` -> `FloatInfo` (sign, raw+unbiased exponent, significand, class, value, ULP) - Classifies normal, denormal, zero, infinity, quiet NaN, signaling NaN - `reinterpretValue` reinterprets low bits as the float value (exact f32->f64) - `nearestBits(format, value)` finds the closest representable pattern and reports whether rounding occurred (detectable for f32) - ULP computed from the exponent (2^(unbiased - significand_bits); subnormals step by the smallest positive subnormal); NaN for inf/NaN - 21 unit tests: 0x3F800000=1.0, special values, denormals, ULP, decimal rounding (3.14 rounds in f32), overflow to inf, NaN handling, round trips - TUI overlay (`src/tui/float_view.zig`, toggled with Ctrl-F in programmer mode): - Color-coded bit grid: sign=pink, exponent=cyan, significand=green - Shows value, classification, formula ((-1)^s x 2^e x mantissa), ULP, and raw fields (sign, biased/unbiased exponent + bias, significand hex) - Ctrl-W toggles f32/f64 and auto-snaps the bit width (32/64) - Typing a decimal float (e.g. `3.14`) stores its nearest bit pattern and notes rounding; non-float input falls back to the integer expression engine - Auto-entry: Tab into programmer mode with a non-integer (or inf/NaN) `ans` opens the float view on the exact f64 bit pattern; integer `ans` uses the integer views - Bit toggling in the grid updates the interpretation live - Verify: `0x3F800000` shows 1.0 (f32), special values classify correctly, `3.14` shows nearest representation with rounding note ### Task 5.4: Implement programmer mode TUI view - bit grid [DONE - merged into 5.3] - Bit grid widget: 8 bits per group, groups separated by space - Arrow key navigation (left/right moves cursor across bits, up/down between rows for 32/64-bit) - Space/Enter toggles the bit under cursor - Bit changes immediately reflected in all base displays - Typing a value in the expression input updates the grid - Toggling bits updates the expression/result displays - Verify: can navigate grid, toggle bits, see values change in real-time ### Task 5.5: Implement struct visualizer TUI view - Create `tui/src/views/struct_viz.zig` - Text input for struct definition (multi-line mini editor or single-line with parsing) - Live-updating field table (offset, size, align, padding) - Memory map visualization (byte grid with color-coded fields and padding) - ABI toggle (`a` key), endian toggle (`e` key) - "Packed comparison" shown at bottom - Transition: from programmer mode, press a key (e.g., `S`) to enter struct sub-view, Esc to return - Verify: can enter struct definition, see layout update, toggle ABI/endian ### Task 5.6: Implement financial mode TUI view - Create `tui/src/views/financial.zig` - Form-style interface: labeled input fields for each financial function - Sub-mode selector: CAGR, Compound Interest, TVM - For TVM: 5 input fields, leave one empty to solve - Result display with formula breakdown - Verify: can input values, compute results, see formula steps ### Task 5.7: Implement unit conversion TUI view [DONE] - Created `src/tui/convert.zig`; convert is now a third top-level mode and Tab cycles Standard -> Programmer -> Convert - Category chips wrap to the terminal width, selected chip highlighted - From/To unit columns list every unit in the category with the selection marked - Live result plus a conversion-factor line (or an "affine conversion" note for temperature, where no single factor applies) - Value comes from the shared input line; a bare number is used directly and anything else is evaluated as a standard expression first (so `2*3.5` works) - Backtick toggles input/selection zone (same convention as programmer mode); arrows navigate columns and selections with wrapping; Ctrl-S swaps units - Changing category resets the unit pair to that category's defaults - Graceful degradation: if the terminal is too short, shows "... N more" - Fully mouse-clickable (see Task 5.9) - Verify: category switching filters units, selection updates the live result, swap works, conversions match the CLI ### Task 5.9: TUI mouse support [DONE] - Requirement documented as FR-7.11 (mouse is first-class, with keyboard parity still required by FR-7.7); architecture in design.md 8.0 - `RegionSet` in `src/tui.zig`: fixed 512-entry table of `{row, col, len, action}` rebuilt every frame during drawing, so hit targets cannot drift out of sync with what is displayed. Newest-registered region wins, letting views layer a row-wide fallback under finer per-digit targets. - `Action` union covers mode tabs, programmer field focus (with optional bit cursor), bit toggling, input focus, help dismissal, float overlay + format, width/endian/signedness toggles, and all convert-mode selections - Only left button press is acted on, so one click fires exactly one action - Wired into every view: mode tabs, standard mode input, programmer mode (bit grid, HEX/OCT digit cursor placement, BIN bit flipping, DEC row focus, Bits/Signed/Endian labels), float view (bits, format/exit hints), convert mode (category chips, both unit columns), and the help overlay - Click semantics follow the data: a bit grid cell and a BIN digit are one bit so clicking flips them; a HEX nibble or OCT digit spans several bits with no single meaningful toggle, so clicking places the cursor for typing - Extracted shared state helpers (`setMode`, `cycleBitWidth`, `toggleEndian`, `toggleSignedness`, `toggleFloatFormat`, `setConvCategory`, `swapConvUnits`) so keyboard and mouse paths cannot diverge - `build.zig`: added a `tui_tests` target rooted at `src/tui.zig`. The CLI test target roots at `src/main.zig`, which only reaches `tui.zig` from `main()`, and `main()` is never analyzed in test mode, so TUI tests were being silently skipped - 12 unit tests for the pure logic: region hit/miss boundaries, newest-wins layering, clear, zero-length rejection, budget exhaustion, action payloads, `wrapIndex` wrapping/clamping, and `defaultUnitIndices` invariants - Verify: `zig build test` runs the TUI tests (366 total); interactive clicking needs a manual pass since the overlay is terminal-only ### Task 5.8: TUI polish and keybindings [PARTIAL] - DONE: `?` help overlay showing keybindings, mouse actions, functions, operators - DONE: mouse support (see Task 5.9): clicking tabs, bits, digits, config labels, convert categories/units, and the input line - DONE: Molokai color theme - NOT DONE: vi-style input mode (optional: Esc for normal mode, i for insert) - NOT DONE: mouse wheel scrolling for history - Verify: help overlay works, mouse interactions work, looks reasonable in 80x24 terminal --- ## Phase 6: Android App ### Task 6.1: Set up Android project structure - Create `android/` directory with Gradle project (Kotlin DSL) - Configure for Compose, minimum SDK 26 (Android 8.0), Material 3 - Create JNI bridge class `TallyEngine.kt` with `external fun` declarations - Set up `jniLibs/` directory structure for arm64-v8a, x86_64 - Native .so files produced by `zig build -Dtarget=aarch64-linux-android` (etc.) - Gradle does not invoke Zig, it consumes prebuilt artifacts - Set up bottom navigation bar (Standard, Programmer, Financial, Convert) - Verify: `zig build -Dtarget=aarch64-linux-android` produces libtally.so; Gradle project builds and empty app launches with nav bar ### Task 6.2: Implement JNI bridge - Kotlin side: `TallyEngine.evaluate(expr, mode, config): String` (returns JSON) - Kotlin side: `TallyEngine.structLayout(definition, abi, endian): String` - Kotlin side: `TallyEngine.convert(value, fromUnit, toUnit): String` - JSON deserialization into Kotlin data classes (Value, MultiBaseResult, StructLayoutResult, ConvertResult, etc.) - Error handling: parse error JSON, surface to UI - Verify: unit tests calling native functions with known inputs, validating deserialized results ### Task 6.3: Implement standard calculator screen - Compose UI: expression display at top (scrollable, natural format), live result preview - Button grid: digits, operators, functions (swipeable function row for sin/cos/log/etc.) - History peek below expression (last 2-3 results), pull-down for full history - ViewModel: manages expression state, calls engine, stores history - Long-press `=` to copy result, tap history items to recall - Verify: can perform calculations, see results, scroll history ### Task 6.4: Implement programmer mode screen - Compose UI: segmented control for bit width (8/16/32/64) - Value display cards: DEC, HEX, OCT, BIN, signed/unsigned (tappable to switch primary input base) - Bit grid: custom Canvas composable, tappable cells with bit index labels, haptic on toggle - Operator button row: AND, OR, XOR, NOT, <<, >>, ROL, ROR - Hex digit buttons (A-F) + numeric keypad + base prefix buttons (0x, 0b) - All displays update instantly on bit toggle or expression entry - FAB/toolbar button navigating to struct layout sub-screen - Verify: bit toggling works, base displays update, expressions evaluate ### Task 6.5: Implement struct visualizer screen - Navigation: accessed from programmer mode via toolbar button - Form-based struct builder: type dropdown + name field per row, add/remove/reorder fields - Toggle for raw DSL text input (for power users / copy-paste from code) - Memory map: custom Canvas composable with color-coded byte bar, tappable segments highlighting field info - Summary card: total size, alignment, padding bytes/percentage, packed comparison - ABI dropdown selector, endian dropdown selector - Verify: can add fields visually, see layout update, toggle between form and raw DSL ### Task 6.6: Implement financial calculator screen - Compose UI: scrollable chip row for function selection (CAGR, TVM, Compound Interest, Present Value) - Card-based input form adapting to selected function - For TVM: 5 labeled input fields, user leaves one blank to indicate solve target - Compute button, result card with animated appearance - Expandable formula breakdown section - Long-press result to copy - Verify: all financial calculations produce correct results, formula display works ### Task 6.7: Implement unit conversion screen - Compose UI: scrollable category chip row at top - Large editable number input (top), from-unit selector dropdown - Swap button (animated rotation) between from/to - Live result display (bottom), to-unit selector dropdown - Numeric keypad (no expression input needed - just digits, decimal, sign) - Unit selectors open searchable bottom sheets - Conversion updates live as user types (no "calculate" button) - Verify: live conversion works, category switching filters units, swap works ### Task 6.8: Android polish - Material 3 theming (dynamic color on Android 12+) - Tablet layout: two-pane (input left, results/visualization right) - Landscape orientation: adapted layouts (wider bit grid, side-by-side panels) - Haptic feedback on bit toggle and button presses - Screen rotation state preservation - App icon, splash screen - Verify: looks correct on phone and tablet, handles rotation, haptics fire --- ## Phase 7: Testing & Release ### Task 7.1: Comprehensive engine test suite [DONE] - **Target: >= 80% line coverage on engine code** -- ACHIEVED: 99.69% (1607/1612 lines) - 222 unit tests passing across engine + CLI modules (208 in the engine coverage run) - Tokenizer tests: all bases, underscores/commas/spaces separators, string literals, invalid/unrecognized characters, bare `<`/`>`, lone dot, huge-decimal float fallback - Parser tests: precedence/associativity, unary ops, function calls (incl. unmatched paren in args), keyword operators, assignment, error paths, infix-position identifier - Evaluator tests: arithmetic, precedence, functions, variables/Ans, string literals in standard mode (ASCII packing + non-ASCII rejection), zero/two/three-arg unknown fns, rand() - Programmer tests: bitwise/shift/rotate at all widths, two's-complement negation, ASCII literal packing + overflow, float-literal truncation, shift-amount >= width clamping - Formatter tests: all bases, all bit widths, scientific notation threshold boundaries - Coverage measured via `zig build coverage` (kcov-based) - Remaining 5 uncovered lines are unreachable defensive branches (parser `tokenToBinaryOp`/ `keywordBinaryOp` fallbacks, gated by `infixPrecedence`) and `or` short-circuits inside test assertions -- not worth contorting the code to reach - Verify: coverage report shows 99.69% on engine modules (well above the >= 80% gate) ### Task 7.2: Integration tests - CLI end-to-end tests (run binary, check stdout/stderr/exit code) - TUI smoke tests (script key sequences, verify screen output via libvaxis test mode if available) - Android instrumented tests for JNI bridge ### Task 7.3: CI/CD setup - Forgejo Actions workflow (at `git.lerch.org/lobo/tally`): - Use mise to provision toolchains (same `.mise.toml` as local dev) - Build + test on Linux, macOS, Windows - Enforce >= 80% engine coverage (fail CI if below) - Cross-compile all targets - Build Android .so files - Release workflow: produce platform binaries as Forgejo release artifacts - Android: produce signed APK/AAB ### Task 7.4: Documentation - `README.md`: project overview, build instructions, usage examples - `CONTRIBUTING.md`: dev setup, architecture overview, how to add an ABI profile - CLI `--help` text finalized - Struct DSL syntax reference (in README or separate doc) --- ## Dependency Graph ``` Phase 1 (foundation) --> Phase 2 (struct + financial) --> Phase 3 (API) | +---------------+---------------+ v v v Phase 4 (CLI) Phase 5 (TUI) Phase 6 (Android) | | | +---------------+---------------+ v Phase 7 (Testing & Release) ``` Phases 4, 5, and 6 can be worked on in parallel once Phase 3 is complete. --- ## Estimated Effort | Phase | Effort | Notes | |-------|--------|-------| | Phase 1 | 2-3 days | Core parsing + eval is the most algorithmic work | | Phase 2 | 2-3 days | Struct layout, financial formulas, unit conversion tables | | Phase 3 | 1 day | Thin wrappers + cross-compile verification | | Phase 4 | 1 day | Straightforward CLI, most logic in engine | | Phase 5 | 3-4 days | TUI widgets, bit grid interaction, struct viz, convert view | | Phase 6 | 4-5 days | Android project setup, Compose UI (5 screens), JNI, polish | | Phase 7 | 2 days | Testing infra, CI, docs | | **Total** | **~15-19 days** | For a single developer working focused |