From 8253766e0f6c187f00c626cf3c73e846e7c75f5d Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Fri, 17 Jul 2026 10:55:08 -0700 Subject: [PATCH] initial commit, including Kiro specs --- .kiro/specs/calculator/design.md | 982 +++++++++++++++++++++++++ .kiro/specs/calculator/requirements.md | 184 +++++ .kiro/specs/calculator/tasks.md | 420 +++++++++++ LICENSE | 21 + 4 files changed, 1607 insertions(+) create mode 100644 .kiro/specs/calculator/design.md create mode 100644 .kiro/specs/calculator/requirements.md create mode 100644 .kiro/specs/calculator/tasks.md create mode 100644 LICENSE diff --git a/.kiro/specs/calculator/design.md b/.kiro/specs/calculator/design.md new file mode 100644 index 0000000..1e5ce64 --- /dev/null +++ b/.kiro/specs/calculator/design.md @@ -0,0 +1,982 @@ +# 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 | `\|` (bitwise OR) | +| 2 | `^` (bitwise XOR) - context-dependent, see below | +| 3 | `&` (bitwise AND) | +| 4 | `<<`, `>>`, `>>>`, `rol`, `ror` | +| 5 | `+`, `-` | +| 6 | `*`, `/`, `%` | +| 7 | `**` or `^` (exponentiation - standard mode) | +| 8 | Unary `-`, `~` (bitwise NOT) | +| 9 | Function calls, parentheses | + +**Context-dependent `^`**: In standard mode, `^` means exponentiation. In programmer mode, `^` means XOR. The parser accepts a mode parameter to resolve this. Programmer mode uses `**` for exponentiation if needed. + +**Implicit multiplication**: The tokenizer detects adjacency patterns (number-identifier, number-paren, paren-paren) and inserts a synthetic `*` token. + +### 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 | `0xFFFF_FFFF` | `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: +- **Value view uses underscore per 16-bit word** (`0xFFFF_FFFF`): matches Zig/Rust literal syntax - this is a programmer tool and values should look like code you'd actually type. Compact, familiar. +- **Memory/byte view uses spaces per byte** (`FF FF FF FF`): matches hex editors, debuggers, `xxd` output. Used in struct visualizer and bit grid context. +- 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). + +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 │ +│ │ +└──── ←→:nav Space:toggle w:width s:sign e:endian y:yank ── Tab:mode ────────┘ +``` + +### 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 │ − │ √ │ │ │ +│ ├─────┼─────┼─────┼─────┼─────┤ │ │ +│ │ 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 111… │ │ +│ │ 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 | Mode as parser parameter | Resolves `^` ambiguity (power vs XOR) at parse time rather than eval time | +| 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 | diff --git a/.kiro/specs/calculator/requirements.md b/.kiro/specs/calculator/requirements.md new file mode 100644 index 0000000..d111a40 --- /dev/null +++ b/.kiro/specs/calculator/requirements.md @@ -0,0 +1,184 @@ +# Requirements: Tally - Cross-Platform Calculator + +## Overview + +A calculator application with three frontends (CLI, TUI, Android) sharing a common Zig engine. Supports standard arithmetic, a programmer mode with struct layout visualization, and financial calculations. + +--- + +## Functional Requirements + +### FR-1: Standard Calculation Mode + +- **FR-1.1**: Parse and evaluate infix mathematical expressions with correct operator precedence (PEMDAS). +- **FR-1.2**: Support operators: `+`, `-`, `*`, `/`, `%` (modulo), `^` (power), unary `-`. +- **FR-1.3**: Support parentheses for grouping. +- **FR-1.4**: Support built-in functions: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `log` (base-10), `ln` (natural), `sqrt`, `cbrt`, `abs`, `ceil`, `floor`, `round`, `factorial`. +- **FR-1.5**: Support constants: `pi`, `e`, `tau`. +- **FR-1.6**: Support variable storage (Ans for last result, named variables A-F, X, Y, Z). +- **FR-1.7**: Maintain calculation history with replay capability. +- **FR-1.8**: Support implicit multiplication (e.g., `2pi`, `3(4+5)`). + +### FR-2: Programmer Mode + +- **FR-2.1**: Accept input in decimal, hexadecimal (`0x`), octal (`0o`), and binary (`0b`) formats. +- **FR-2.2**: Simultaneously display results in all four bases (dec, hex, oct, bin). +- **FR-2.3**: Support configurable bit widths: 8, 16, 32, 64-bit. +- **FR-2.4**: Support bitwise operators: AND (`&`), OR (`|`), XOR (`^`), NOT (`~`), left shift (`<<`), right shift (logical `>>>`), arithmetic right shift (`>>`), rotate left (`rol`), rotate right (`ror`). +- **FR-2.5**: Display both signed (two's complement) and unsigned interpretations of the current value. +- **FR-2.6**: Visualize the bit pattern as a grid (integer.exposed style) - bits individually addressable/toggleable in TUI and Android. +- **FR-2.7**: Quick toggle between base display formats via dedicated keyboard shortcuts (TUI). + +### FR-3: Struct Layout Visualizer (Programmer Mode Extension) + +- **FR-3.1**: Accept struct definitions in a mini-DSL: + ``` + struct { + u32 x; + u8 flags; + u16 id; + u64 timestamp; + } + ``` +- **FR-3.2**: Compute and display total struct size in bytes. +- **FR-3.3**: Display per-field information: offset, size, alignment requirement. +- **FR-3.4**: Identify and highlight padding bytes between fields and at the end (tail padding). +- **FR-3.5**: Render a byte-level memory map showing which bytes belong to which field and which are padding. +- **FR-3.6**: Default to x86-64 System V ABI alignment rules. +- **FR-3.7**: Support switching ABI/alignment profiles (extensible design - future: Windows x64, ARM AAPCS, packed). +- **FR-3.8**: Support switching endianness display (little-endian default, big-endian available). +- **FR-3.9**: Supported field types (initial set): `u8`, `u16`, `u32`, `u64`, `i8`, `i16`, `i32`, `i64`, `f32`, `f64`, `bool`, `ptr` (pointer-sized, arch-dependent). +- **FR-3.10**: Show a "packed" comparison - display what the struct would look like with no padding vs. natural alignment. + +### FR-4: Unit Conversions + +- **FR-4.1**: Support inline unit conversion syntax: ` to ` (e.g., `100 km to miles`, `72 F to C`). +- **FR-4.2**: Support unit conversions within expressions: `5 kg + 3 lb` evaluates in the left-hand unit. +- **FR-4.3**: Length: mm, cm, m, km, in, ft, yd, mi, nm (nautical mile). +- **FR-4.4**: Mass/Weight: mg, g, kg, oz, lb, ton, tonne. +- **FR-4.5**: Temperature: C (Celsius), F (Fahrenheit), K (Kelvin). +- **FR-4.6**: Time: ms, s, min, hr, day, week, year. +- **FR-4.7**: Digital storage: bit, byte, KB, MB, GB, TB, PB (both SI and binary: KiB, MiB, GiB, TiB). +- **FR-4.8**: Speed: m/s, km/h, mph, knots. +- **FR-4.9**: Area: mm2, cm2, m2, km2, in2, ft2, acre, hectare. +- **FR-4.10**: Volume: ml, L, gal, qt, pt, fl_oz, cm3, m3. +- **FR-4.11**: Energy: J, kJ, cal, kcal, Wh, kWh, BTU. +- **FR-4.12**: Pressure: Pa, kPa, bar, atm, psi, mmHg. +- **FR-4.13**: Data rate: bps, Kbps, Mbps, Gbps, B/s, KB/s, MB/s, GB/s. +- **FR-4.14**: Angle: deg, rad, grad, turn. +- **FR-4.15**: In CLI, support `tally "100 km to miles"` and `tally convert 100 km miles`. +- **FR-4.16**: In TUI, dedicated conversion panel accessible from any mode. +- **FR-4.17**: Unit system extensible - adding a new category requires only a conversion table, no parser changes. + +### FR-5: Financial Mode + +- **FR-5.1**: Compute CAGR given start value, end value, and number of periods. +- **FR-5.2**: Compute compound interest (future value given principal, rate, periods, compounding frequency). +- **FR-5.3**: Compute present value (discount future cash flow). +- **FR-5.4**: Support TVM (Time Value of Money) solver - given any 4 of: N, I/Y, PV, PMT, FV, solve for the 5th. +- **FR-5.5**: Display intermediate calculation steps/formula used (transparency). + +### FR-6: CLI Frontend + +- **FR-6.1**: Invoke as `tally ""` for standard mode evaluation. +- **FR-6.2**: Flag `--programmer` or `-p` for programmer mode: `tally -p "0xFF & 0x0F"`. +- **FR-6.3**: Flag `--bits <8|16|32|64>` to set bit width in programmer mode (default: 64). +- **FR-6.4**: Flag `--base ` to control primary output format (default: show all). +- **FR-6.5**: Subcommand `tally struct ` for struct layout - accepts inline DSL string or path to a file containing the definition. +- **FR-6.6**: Subcommand `tally cagr ` for quick CAGR. +- **FR-6.7**: Subcommand `tally tvm` with named flags (`--pv`, `--fv`, `--n`, `--rate`, `--pmt`) - omit one to solve for it. +- **FR-6.8**: Subcommand `tally convert ` for unit conversion. +- **FR-6.9**: Output format flags: `--json` for machine-readable output, plain text default. +- **FR-6.10**: Exit code 0 on success, non-zero on parse/evaluation errors with stderr message. + +### FR-7: TUI Frontend + +- **FR-7.1**: Full-screen terminal UI with mode tabs (Standard, Programmer, Financial, Convert). +- **FR-7.2**: Standard mode: expression input line, result display, scrollable history. +- **FR-7.3**: Programmer mode: bit grid (navigable with arrow keys, toggle with Space/Enter), simultaneous base displays, expression input. +- **FR-7.4**: Programmer mode struct sub-view: field list editor, live-updating memory map visualization. +- **FR-7.5**: Financial mode: form-style input for parameters, result display with formula breakdown. +- **FR-7.6**: Convert mode: select category, input value, select from/to units, live result. +- **FR-7.7**: Keyboard-driven navigation; no mouse required (mouse optional enhancement). +- **FR-7.8**: Quick-switch keys for base display in programmer mode (e.g., `d`=dec, `h`=hex, `o`=oct, `b`=bin to highlight primary). +- **FR-7.9**: Support terminal resize gracefully. +- **FR-7.10**: Vi-style and Emacs-style keybinding options for expression input. + +### FR-8: Android Frontend + +- **FR-8.1**: Native Android app using Kotlin and Jetpack Compose. +- **FR-8.2**: Call into the Zig engine via JNI (C ABI shared library). +- **FR-8.3**: Bottom navigation bar for mode switching (Standard, Programmer, Financial, Convert). +- **FR-8.4**: Standard mode: traditional calculator layout with button grid, expression display at top, result below. +- **FR-8.5**: Programmer mode: tappable bit grid, segmented control for bit width, base display cards, operator button row for bitwise ops. +- **FR-8.6**: Programmer mode struct sub-screen: text field for struct input, scrollable field table, color-coded memory map rendered as a custom Canvas composable. +- **FR-8.7**: Financial mode: card-based UI with input fields, dropdown for function selection (CAGR, TVM, etc.), compute button, result card with formula. +- **FR-8.8**: Convert mode: category picker (horizontal chips), from/to unit selectors, live conversion as user types. +- **FR-8.9**: Responsive layout for phone and tablet (multi-pane on tablet). +- **FR-8.10**: Follow Material Design 3 guidelines with dynamic color (Android 12+). +- **FR-8.11**: Haptic feedback on bit toggle and button presses. +- **FR-8.12**: Support landscape orientation with adapted layouts (wider bit grid, side-by-side panels). + +--- + +## Non-Functional Requirements + +### NFR-1: Performance + +- Expression parsing and evaluation must complete in < 1ms for typical expressions. +- TUI must render at >= 30fps during interaction (bit toggling, navigation). +- Struct layout computation must handle structs with up to 256 fields without perceptible delay. + +### NFR-2: Portability + +- Engine, CLI, and TUI must compile and run on: Linux (x86-64, aarch64), macOS (aarch64, x86-64), Windows (x86-64). +- Android engine .so must target: arm64-v8a, armeabi-v7a, x86_64. +- Single `zig build` invocation to produce all desktop targets (cross-compilation). + +### NFR-3: Correctness + +- Floating-point arithmetic uses IEEE 754 double precision (64-bit). +- Programmer mode integer operations must be exact (no floating-point). +- Struct layout computation must match the actual layout produced by a C compiler under the specified ABI. + +### NFR-4: Extensibility + +- Adding a new ABI profile for struct layout requires implementing a single interface (alignment rules + pointer size). +- Adding new financial functions requires no changes to the parser - use a function-call syntax. +- Adding new unit categories/units requires only table entries, no parser changes. +- Engine exposes a stable C ABI for future frontends (e.g., WASM, iOS). + +### NFR-5: Testability + +- Engine must be testable in isolation (no I/O dependencies). +- Struct layout results must be verifiable against `pahole` or compiler output for reference structs. +- Engine test coverage target: >= 80% line coverage. The engine is pure computation with no I/O, so this is achievable and expected. +- Frontend coverage: best-effort via integration and smoke tests (TUI key-sequence tests, Android instrumented tests). No hard coverage target on frontend code. +- All public engine API functions must have corresponding unit tests covering happy path, error cases, and edge cases. + +### NFR-6: Build & Distribution + +- **Zig build system for everything.** A single `zig build` invocation at the workspace root handles all targets: engine, CLI, TUI, and Android .so cross-compilation. No Gradle required for the native library build - Gradle only wraps the prebuilt .so into the APK. +- **Mise for toolchain management.** All required toolchains (Zig version, Android NDK if needed for validation) are declared in `.mise.toml` at the project root. A developer needs only `mise` installed - running `mise install` provisions everything else. +- **No other system-level dependencies.** Outside of mise, a developer should not need to manually install Zig, Android SDK/NDK, or any other toolchain. The `.mise.toml` is the single source of truth for tool versions. +- **libvaxis for TUI.** The TUI frontend uses libvaxis (pulled as a Zig build dependency). No other TUI framework. +- Produce statically-linked binaries for CLI and TUI (no shared library dependencies on end-user system). +- Android: produce .so files for arm64-v8a, x86_64 via `zig build -Dtarget=aarch64-linux-android` (or similar). The Gradle project in `android/` consumes these prebuilt artifacts. +- Minimal runtime dependencies: Zig stdlib + libvaxis (TUI only). No other native deps. + +### NFR-7: Number Display & Formatting + +- Decimal numbers must use comma grouping for display (e.g., `4,294,967,295`). +- Scientific notation only when value exceeds 15 significant digits or absolute value > 10^15 / < 10^-15. Never jump to scientific notation for values that fit in a readable decimal. +- Programmer mode hex values display with underscore grouping per 16-bit word (e.g., `0xFFFF_FFFF`). +- Programmer mode binary values display grouped by nibble with spaces (e.g., `1111 1111`). +- All frontends must distinguish between "display format" (with separators) and "clipboard format" (raw, no separators). +- Android: long-press any result to copy the raw (no-separator) value to clipboard. +- TUI: yank command copies raw value to system clipboard. +- CLI: stdout output uses display format by default; `--raw` flag or `--json` gives unformatted values. + +### NFR-8: Security + +- C API must not use null-terminated strings. All string inputs take `(pointer, length)` pairs to prevent buffer overread. +- Engine must validate all input lengths before processing. +- No unbounded allocations from untrusted input (enforce maximum expression length, maximum struct field count). diff --git a/.kiro/specs/calculator/tasks.md b/.kiro/specs/calculator/tasks.md new file mode 100644 index 0000000..bb2b550 --- /dev/null +++ b/.kiro/specs/calculator/tasks.md @@ -0,0 +1,420 @@ +# 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 +- 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 + +### Task 1.2: Implement core types module +- 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 +- 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 `_` as digit separator in number literals (e.g., `1_000_000`, `0xFF_FF`) +- Verify: unit tests for tokenizing expressions in all bases, edge cases (negative numbers, adjacent tokens for implicit mult) + +### Task 1.4: Implement Pratt parser +- 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 +- Create `engine/src/evaluator.zig` +- Create `engine/src/environment.zig` with `Environment` struct (variables, history, config) +- 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 +- Extend evaluator for `Mode.programmer` +- 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 +- 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.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 +- Create `engine/src/units.zig` +- Define `UnitCategory` enum and `UnitDef` struct (name, aliases, category, conversion factors) +- Implement conversion tables for all categories: length, mass, temperature, time, digital storage, speed, area, volume, energy, pressure, data rate, angle +- Implement `convert(value: f64, from: UnitDef, to: UnitDef) !f64` +- Temperature special case: formula-based conversion (C/F/K with offsets) +- Implement unit name resolution: given a string, find the matching `UnitDef` (supports aliases) +- Verify: unit tests for all conversion categories, round-trip accuracy, unknown unit error handling + +--- + +## 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 +- Create `cli/src/main.zig` +- Parse args: positional expression, `-p`/`--programmer`, `--bits`, `--base`, `--json`, `--help`, `--version` +- Parse subcommands: `struct`, `cagr`, `tvm`, `convert` +- For `tvm`: parse `--n`, `--rate`, `--pv`, `--pmt`, `--fv` (detect which is missing -> solve for it) +- For `convert`: parse `tally convert ` +- Also support inline conversion in expressions: `tally "100 km to miles"` +- Verify: unit tests for arg parsing edge cases; `--help` prints usage + +### Task 4.2: Implement CLI output formatting +- Standard mode: print result (plain number or formatted float) +- Programmer mode: print multi-base table (dec signed, dec unsigned, hex, oct, bin, bit pattern) +- Struct mode: print table with offsets/sizes/padding + memory map ASCII art +- Financial mode: print result + formula used +- `--json` flag: output structured JSON for all modes +- Verify: golden-file tests comparing output against expected strings + +### Task 4.3: CLI integration and error handling +- Wire arg parser -> engine calls -> output formatter +- Map `CalcError` to user-friendly stderr messages with position indicators +- Set exit codes: 0 success, 1 parse error, 2 eval error +- Verify: end-to-end tests with both valid and invalid inputs + +--- + +## Phase 5: TUI Frontend + +### Task 5.1: Set up TUI skeleton +- Create `tui/src/main.zig` with app initialization, event loop, graceful shutdown (libvaxis already wired in Task 1.1) +- Implement mode tab bar (Standard / Programmer / Financial / Convert) with Tab key switching +- Implement basic window frame and resize handling +- Verify: TUI launches, displays tabs, responds to Tab and q (quit) + +### Task 5.2: Implement standard mode TUI view +- Create `tui/src/views/standard.zig` +- Expression input line with cursor (basic line editing: insert, delete, left/right, home/end) +- Result display below input +- Scrollable history panel above input +- Wire input -> engine.evaluate() -> display result + push to history +- Verify: can type expressions, see results, scroll history + +### Task 5.3: Implement programmer mode TUI view - expression & base display +- Create `tui/src/views/programmer.zig` +- Expression input (same as standard but in programmer mode) +- Multi-base result display (dec signed, unsigned, hex, oct, bin) +- Quick-toggle keys: `d` highlights dec, `h` highlights hex, `o` oct, `b` bin +- Status bar showing current bit width, signedness, endianness +- Width toggle (`w` key cycles 8->16->32->64), sign toggle (`s`), endian toggle (`e`) +- Verify: expressions evaluate in programmer mode, base display updates, toggles work + +### Task 5.4: Implement programmer mode TUI view - bit grid +- 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 +- Create `tui/src/views/convert.zig` +- Category selector (navigable list or hotkeys) +- From-value input field, from-unit selector +- To-unit selector, live-updating result +- Quick swap (from ↔ to) key +- Show conversion factor used +- Verify: can select category, units, enter value, see live result update + +### Task 5.8: TUI polish and keybindings +- Implement `?` for help overlay showing all keybindings +- Implement vi-style input mode (optional: Esc for normal mode, i for insert) - configurable +- Mouse support: click on bit grid to toggle, click on tabs to switch modes +- Color theme (sensible defaults, respects terminal capabilities) +- 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 +- **Target: >= 80% line coverage on engine code** +- Property-based tests for parser (roundtrip: parse -> format -> parse) +- Known-answer tests for all financial functions (textbook values) +- Unit conversion round-trip tests (convert A->B->A, verify precision) +- Struct layout verification against `pahole` output for 20+ test structs +- Fuzz testing for parser (random input shouldn't crash, should return error) +- Edge cases: max values, min values, NaN, infinity, division by zero +- Formatter tests: all bases, all bit widths, scientific notation threshold boundaries +- Coverage measured via `zig build test` with coverage reporting enabled +- Verify: coverage report shows >= 80% on engine modules + +### 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 | diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2252f67 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Emil Lerch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.