initial commit, including Kiro specs

This commit is contained in:
Emil Lerch 2026-07-17 10:55:08 -07:00
parent 7a27b24c7a
commit 8253766e0f
Signed by: lobo
GPG key ID: A7B62D657EF764F8
4 changed files with 1607 additions and 0 deletions

View file

@ -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 `<expr> <unit> to <unit>`:
- 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] <expression>
tally struct [OPTIONS] <definition | --file path>
tally convert <value> <from_unit> <to_unit>
tally cagr <start_value> <end_value> <periods>
tally tvm [--n N] [--rate R] [--pv PV] [--pmt PMT] [--fv FV]
Options:
-p, --programmer Programmer mode
-b, --bits <WIDTH> Bit width: 8, 16, 32, 64 (default: 64)
--base <BASE> Output base: dec, hex, oct, bin, all (default: all)
--json JSON output
--abi <ABI> Struct ABI: sysv (default), packed
--endian <E> 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 |

View file

@ -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: `<value> <from_unit> to <to_unit>` (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 "<expression>"` 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 <dec|hex|oct|bin>` to control primary output format (default: show all).
- **FR-6.5**: Subcommand `tally struct <definition_or_file>` for struct layout - accepts inline DSL string or path to a file containing the definition.
- **FR-6.6**: Subcommand `tally cagr <start> <end> <periods>` 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 <value> <from_unit> <to_unit>` 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).

View file

@ -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=<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
- 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 <value> <from_unit> <to_unit>`
- 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 |

21
LICENSE Normal file
View file

@ -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.