tally/.kiro/specs/calculator/tasks.md
2026-07-28 07:06:01 -07:00

40 KiB

Tasks: Tally - Cross-Platform Calculator

References


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 [DONE]

  • ast.Expr.Number gains text: []const u8 (the literal's source text). Additive, so the 10 parser assertions on float_value/int_value keep compiling. The text is needed because float_value has ALREADY rounded by the time it exists: 0.1 cannot be recovered from its binary approximation, so exact evaluation has to re-parse the literal.
  • evaluator.evaluate keeps its CalcError!f64 signature but now runs on Number internally via evalExact, collapsing to f64 once at the boundary. A scratch arena per evaluation keeps Number lifetimes trivial and means the caller's allocator (an arena in the CLI, a GPA in the TUI) never holds intermediates.
  • Exact: + - * / %, integer powers, sqrt of perfect squares, abs, floor, ceil, round, factorial, max, min, and all literals.
  • Inexact: transcendentals, fractional powers, sqrt of non-squares, cbrt, atan2, two-arg log, rand, and the constants pi/e/tau.
  • Fixed-width integer operators (& | xor << >> >>> rol ror, ~) stay on the float/integer projection: they are not rational arithmetic.
  • Added to rational.zig: floor, ceil, round, mod (the a - b*floor(a/b) definition), and unbounded exact factorial.
  • Added to number.zig: the matching wrappers plus max/min.
  • rational.Error.ExponentTooLarge maps to CalcError.Overflow.
  • ALL 489 pre-existing tests pass unchanged. 522 total now (+33).
  • Verified through the unchanged f64 API: 0.1 + 0.2 = 0.3, 1.1 + 2.2 = 3.3, 0.1 * 3 = 0.3, 0.1+0.2+0.3 = 0.6, (0.1+0.2)*10-3 = 0, 1e20 + 1 - 1e20 = 1 (f64 gives 0), 1/3*3 = 1, factorial(171) no longer errors. Unchanged: 2 + 3 * 4, sqrt(144), sqrt(2), sin(0), e, 0o777 - 0x0f, 10 % 3, 0xFF & 0x0F.
  • Known limitations deferred to 2.0c/2.0e:
    • Variables and Ans are still stored as f64 in Environment, so X = 0.1 round-trips through a float. Correct for pi/e/tau (irrational), a real limitation for user variables.
    • 9007199254740993 and 2^53 + 1 are computed exactly but cannot survive the f64 return. That is precisely what 2.0c fixes.
    • 12 in in ft is NOT fixed: unit conversion never touches the evaluator and its factors are already-rounded f64 values. See Task 2.0e.
  • Test-oracle bug found by the instrumented coverage build: Zig's float @mod with a NEGATIVE divisor returned -2 in the normal build and 1 under the coverage build (comptime folding and the runtime path disagree). A test whose expected value shifts with optimize mode verifies nothing, so the mod test now asserts the values the definition requires instead of deriving them from @mod. Worth reporting upstream.
  • Allocation-failure sweeps added using std.testing.FailingAllocator (fail_index = fail after N allocations) over testing.allocator, so leaks are detected rather than the cleanup lines merely being executed. These found three real bugs unreachable by ordinary tests: a double free (initOwned freed values its callers also freed), a potential dangling copy (parts passed by value while normalization can reallocate limbs), and struct-literal leaks in initInt / initZero / initRatio where an errdefer cannot cover a value that does not exist yet. floor and factorial were also missing a denominator errdefer. See design.md 2.7.10. Uncovered lines in the numeric modules went 25 -> 8.

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.0e: Exact unit conversion factors [NOT STARTED]

Discovered while implementing 2.0b: making the evaluator exact does NOT fix 12 in in ft = 0.9999999999999998, because unit conversion never goes through the evaluator. UnitDef.to_base_factor is an f64, so 0.0254 is already the binary approximation before convertUnits does its own f64 multiply and divide.

  • Declare factors as exact decimal text (e.g. "0.0254") so they can be parsed into rationals; an inch is exactly 127/5000 m and a foot exactly 381/1250 m, which makes 12 in to ft exactly 1.
  • Provide an exact conversion path returning Number, keeping the f64 path for callers that want it.
  • Mechanical across ~100 table entries, hence its own commit: the existing invariant tests (every unit and unit pair round-trips) become far stronger when the round trip is exact rather than within a tolerance.
  • Non-terminating conversions (100 km to mi = 781250/12573) still render as rounded decimals, but from an exact value, and the exact fraction becomes available for display.

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 <value> <unit> to <unit> 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=<triple>
  • 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 <value> <from> [to] <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