tally/.kiro/specs/calculator/tasks.md

13 KiB

Tasks: Tally - Cross-Platform Calculator

References

Open work only. Each item has a name rather than a number, so finishing one renumbers nothing; when an item is done, delete it. The record of what was done and why is history.md, where every finished task and numbered open item from before this file was split still lives under the name the code and the other documents cite it by ("Task 5.19", "open item 10").


Engine

Ans is a pseudovariable, and the engine models it as a constant

FR-1.6.1; was open item 16. Task 5.20 stopped Ans = 5 from being silently discarded, but did it by folding Ans in with pi, e and tau behind one Environment.isBuiltIn, and the error it raises is AssignmentToConstant, "cannot assign to a built-in constant". Ans is not a constant: it changes on every evaluation.

  • The name and the message. A name the engine writes and the user only reads wants its own error, or a shared one worded to cover both, so Ans = 5 does not claim Ans is constant.
  • The read path. getVar special-cases the two spellings inline, ahead of the variable map and next to the constant table. A pseudovariable is a third kind of name and reads as one only if it is declared as one.

Scope is the immediately preceding answer and nothing else: no Ans2, no indexed history. Earlier results stay FR-1.7's replay.

Unit arithmetic inside expressions

FR-4.2: 5 kg + 3 lb evaluates in the left-hand unit. Unlike FR-4.1, which needs only the to keyword as a trigger, this needs unit-aware tokenizing and evaluation. Design 4.2 sketches the approach.

The 40-digit display cap is provisional

NFR-7: the integer-digit cap above which an exact value's display abbreviates to scientific notation is "PROVISIONALLY 40 digits, pending review", chosen as the smallest round number above 2^128 and not from any measured preference. It is now each frontend's FormatOptions.max_integer_digits (Task 5.17), so the review is per frontend: the CLI and the TUI both say 40, the Android calculator its own.


Display and clipboard

The exact fraction is never shown

Was open item 13. NFR-9.9 ends with "the exact fraction available", and no frontend asks for it. Rational.toFractionString works and is tested, but its only callers are tests: nothing renders 1/3 as a fraction. Wiring it needs a display decision that does not exist yet: integers and inexact values have no useful fraction form, so the frontend has to choose when to show the line at all.

Copying a result, and marking one approximate

Was open item 14. NFR-7 asks for a raw (unseparated) form for the clipboard on every frontend, and NFR-9.9 for approximate results to be marked. Android has both: a long-press copies, and the converter marks inexact rows. The terminal frontends have neither:

  • The TUI has no yank command and draws no approximate marker, though Number.render reports truncated and takes clipboard options. display_format's doc comment in src/tui.zig points at a clipboard_format that does not exist.
  • The CLI has no --raw (NFR-7). FR-6.9 removed --json until it can print the C ABI's encoding rather than a second one of its own.

TUI

Bracketed paste submits a line at a time

Was open item 20. vxfw turns bracketed paste on, and the TUI drops paste_start and paste_end (typeErasedEventHandler's else), so a pasted newline arrives as Enter: pasting a two-line expression evaluates the first line and leaves the second in the prompt. Small and mode-independent: while a paste is open, Enter inserts a space. The struct view needs it first, since a pasted struct is several lines.

History stops showing new results, and never frees what it held

Was open item 8, with its other half from item 18. drawHistory flattens the history oldest-first into 512 line slots and slices the tail, so results stop appearing after roughly a hundred entries with detail lines. The fix is small now: fill the window newest-first, bounded by the rows actually visible, so the cap limits how far back a very tall terminal can show rather than how long a session can run. History memory is also never reclaimed: Ctrl-L frees into an arena. Task 5.8 also left the mouse wheel unable to scroll history.

Programmer mode's value zone

From open item 18, and FR-7.3's note.

  • ProgField.expression is a dead focus stop: Up/Down cycles through it, but no row is drawn for it, no key does anything in it (handleValueInput has an empty prong, fieldBitStep returns 0) and nothing highlights it. Drop it, or make it hand focus back to the input line, which is what backtick and focus_input already do.
  • The decimal fields cannot be corrected or signed. There is no backspace, and handleValueInput treats DEC(s) and DEC(u) alike as unsigned multiply-and-add, so typing into DEC(s) while it shows a negative number operates on the two's complement bits. There is no way to type a minus sign.
  • The value-zone status hint advertises Enter:set, and Enter does nothing there; Space is the only toggle (FR-7.3).

Logic src/tui.zig holds twice

From open item 18.

  • The bit grid's geometry: handleValueZoneKey computes bits_per_row = if (width > 32) 32 else width for Up/Down, and programmer.zig's drawBitGrid the same for drawing. Change one and the arrows disagree with the grid.
  • The float-view toggle: applyAction's .toggle_float and handleKey's Ctrl-F both flip float_view_active, call syncFloatWidth and set prog_field = .bits, and the bit cursor is clamped three times over. One toggleFloatView would hold it.
  • display_format is the same five numbers in src/tui.zig and src/cli/format.zig, with the same rationale. Per-frontend choice is the requirement (NFR-9.9), so this may be right, but it should be a decision rather than a coincidence.
  • Smaller: the file header lists three of the six view files it imports; App.env and init spell engine.evaluator.Environment where engine.Environment exists; modeIndex wraps @intFromEnum for two callers; Mode is private while Action, nextMode and prevMode are public and name it; loadAnsIntoProgrammer bounds its integer test with the literals -9223372036854775808.0 and 18446744073709551616.0; and handleKey is a 200-line if-chain where a new binding can land in the wrong order.

Themes

FR-7.9.3; was open item 17. src/tui/draw.zig declares C, eleven Molokai colours as literal RGB triples, and every view reads them directly (C.cyan, C.pink, C.sel), so the palette is a compile-time fact: no light theme, no following the terminal's own colours, no user choice. The names are also the palette's rather than the interface's: views ask for C.pink where they mean "an error" and C.yellow where they mean "the focused field".

Two steps, in order: name the roles the views use (error, focus, selection, muted, accent), then make the mapping from role to colour a value the app holds rather than a set of constants. Only then is the palette data - a default, a file or a flag. Doing the second without the first just moves the same eleven names behind a pointer.

Vi- and Emacs-style input

FR-7.10, and Task 5.8's remainder: the prompt is a plain text field. Deferred, not dropped.


Struct layout (on trial)

Stage 1, the engine and tally struct, is Task 2.1. In rough order of value:

Use it, then decide

If tally struct is not reached for, the feature comes out whole: two engine files, the clang check (engine/test/struct_oracle.zig and its block in build.zig), the subcommand, and FR-3. That is easier before anything else depends on it.

The TUI view

FR-7.4. Decided while planning it, before it was deferred: a programmer sub-view on Ctrl-S rather than a fifth mode, with float_view_active becoming a three-way prog_view enum so the two sub-views cannot both be on; and bracketed paste fixed first. The CLI's table and map (design 7) are a better starting point than the sketch in design 8.3, not least because the TUI draws one byte per cell and cannot show box or block glyphs. The editing model (paste a definition, add a member, reorder to watch the padding move) is a guess until the CLI has been used.

A C ABI export, then the Android screen

tally_struct_layout, with JSON shaped like the CLI's table, then FR-8.6 and design 9.4.

Smaller follow-ups

  • Decoding bytes through a struct, which is where endianness (FR-3.8) earns its place: a hex string, or the programmer register, read as each member's value.
  • Rust #[repr(C)] and Zig extern struct front ends (FR-3.13). Zig's own @offsetOf would be the oracle for the second.
  • A "reorder by alignment" line: the size of the same members sorted by descending alignment, which is the question padding usually prompts.
  • Types left out by decision rather than difficulty, each needing clang cases of its own: long double (which would split sysv64, since it is 16 bytes on x86-64 and 8 on AArch64 macOS), _Atomic (which raises alignment on i386), and a bare aligned (the target's largest alignment, which no profile records).
  • An i386-Windows profile: MSVC aligns 8-byte types to 8 there, unlike i386 Linux, and it would exercise the Microsoft rules on a 32-bit target.
  • Out of scope unless a real need appears (FR-3.12): nested structs and unions, bitfields, typedefs and macros from elsewhere. That is where a C front end turns into a C compiler, and zig cc is already one.

Android

The financial screen

FR-8.7, design 9.5. Needs C ABI payloads for the forms and the amortization schedule first (Task 6.0 left them open: "better decided against a screen than in advance"). Design 9.5's wireframe has a Calculate button, which the TUI dropped because its results are live; expect it to be redrawn from a running screen, as 9.2 was. The plan as first written:

  • A scrollable chip row for the calculation: CAGR, TVM, compound interest, present value.
  • A card-based form adapting to it; for TVM, five fields with the blank one solved for.
  • A result card with an expandable formula breakdown, and long-press to copy.

The rest of programmer mode

Task 6.4 shipped a first version without history, the float view, ASCII literals ('ELF') and the system keyboard. None needs the engine to change except the float view, which needs a float-view payload in the C ABI (Task 6.0).

Polish

FR-8.9, FR-8.11, FR-8.12 and the rest of Task 6.8:

  • A two-pane layout for tablets (input left, results right).
  • Landscape layouts: a wider bit grid, side-by-side panels.
  • Haptic feedback on bit toggles and key presses.
  • A splash screen.
  • sqrt drawn as a radical in the calculator's expression, rather than as sqrt( (design 9.2).
  • Rotation: check that every screen survives it. State is saved on every edit, which should cover it, but nobody has rotated each screen and looked.

Checks not yet made on a device

TalkBack on every screen, though every symbol key carries a spoken label; a real on-screen keyboard's dismissal bringing the pad back on the calculator (the emulator's IME showed its hardware-keyboard toolbar, so only Enter was exercised); the light theme on the converter after its palette change; and the converter migrating the single value an older build saved, which is unit tested and has never run on a device (an instrumented run uninstalls the app first).


Build, CI and release

Windows and Intel macOS are built by nobody and run by nobody

NFR-2 names Windows x86-64 and macOS x86-64 alongside the targets CI builds (x86_64 and aarch64 Linux, aarch64 macOS). zig build -Dtarget=x86_64-windows-gnu and -Dtarget=x86_64-macos both compile today; neither is in CI's release builds, and neither binary, nor the TUI on a Windows console, has ever been run.

Tests that run the binary

Task 7.2's first item: nothing runs tally and checks its stdout, stderr and exit status. Every CLI test calls the formatters directly, which is how redirected output was overwriting its own first byte for as long as it was (Task 5.30): the defect lived in main, between the formatter and the file, where no test looked. A test that runs the binary with stdout sent to a file would have caught it. Probably a build step that runs the installed binary over a table of invocations, the way jvm-test runs Java.

The coverage gate runs only before a commit

Task 7.3: zig build coverage -Dcoverage-threshold=80 is a pre-commit hook and not a CI step ("wttr.in has the same step disabled in CI; not investigated").

The TUI's Cobertura report loses src/tui.zig

kcov writes the TUI report's sources relative to src/tui/, and src/tui.zig sits outside that root, so cobertura.xml names it zig. Queries by path find nothing for the file with most of the TUI's code; sonarqube.xml in the same report has the absolute path and answers correctly. Either the report's source root (build/Coverage.zig) or the include paths could fix it.


Documentation

CONTRIBUTING.md

Task 7.4: dev setup, an architecture overview, and how to add an ABI profile (now a table entry in struct_layout.zig plus clang targets in build.zig). The struct syntax reference is tally struct --help and FR-3.1/FR-3.12, and wants a README section if the feature stays.