tally/.kiro/specs/calculator/tasks.md
Emil Lerch 850378840b
All checks were successful
Build / Android (push) Successful in 4m42s
Build / Engine, CLI and TUI (push) Successful in 4m56s
Build / Sign (push) Successful in 5m7s
Build / Publish (push) Successful in 19s
Build / F-Droid repo (push) Successful in 1m4s
add windows and mac/intel to the build
2026-10-14 08:23:15 -07:00

287 lines
14 KiB
Markdown

# Tasks: Tally - Cross-Platform Calculator
## References
- #[[file:.kiro/specs/calculator/requirements.md]]
- #[[file:.kiro/specs/calculator/design.md]]
- #[[file:.kiro/specs/calculator/history.md]]
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
### The Intel macOS and Windows on ARM builds, and the TUI on Windows, have never run
CI builds and publishes `tally-x86_64-macos`, `tally-x86_64-windows.exe` and
`tally-aarch64-windows.exe` with the other desktop targets. The Windows CLI has been run
under Wine: arithmetic, programmer mode, conversion, `tally struct` from an argument and
from stdin, errors with exit status 1, and output redirected to a file and appended to.
Still never run: the Intel macOS binary at all (it is a well-formed x86_64 Mach-O, which
is all that has been checked), the ARM64 Windows binary at all (an ARM64 PE that, like
the x64 one, imports only `ntdll.dll` and `kernel32.dll`; Wine here does not run ARM64),
and the TUI on a real Windows console. Under Wine with no console it fails cleanly with
"failed to start the TUI: InvalidHandle", which says nothing about a console that exists.
### 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.