tally/.kiro/specs/calculator/design.md

45 KiB
Raw Blame History

Design: Tally - Cross-Platform Calculator

References


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

/// 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

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:

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

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

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

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.

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:

// 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)

// 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

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