13 KiB
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 = 5does not claimAnsis constant. - The read path.
getVarspecial-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.renderreportstruncatedand takes clipboard options.display_format's doc comment insrc/tui.zigpoints at aclipboard_formatthat does not exist. - The CLI has no
--raw(NFR-7). FR-6.9 removed--jsonuntil 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.expressionis a dead focus stop: Up/Down cycles through it, but no row is drawn for it, no key does anything in it (handleValueInputhas an empty prong,fieldBitStepreturns 0) and nothing highlights it. Drop it, or make it hand focus back to the input line, which is what backtick andfocus_inputalready do.- The decimal fields cannot be corrected or signed. There is no backspace, and
handleValueInputtreats 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:
handleValueZoneKeycomputesbits_per_row = if (width > 32) 32 else widthfor Up/Down, andprogrammer.zig'sdrawBitGridthe same for drawing. Change one and the arrows disagree with the grid. - The float-view toggle:
applyAction's.toggle_floatandhandleKey's Ctrl-F both flipfloat_view_active, callsyncFloatWidthand setprog_field = .bits, and the bit cursor is clamped three times over. OnetoggleFloatViewwould hold it. display_formatis the same five numbers insrc/tui.zigandsrc/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.envandinitspellengine.evaluator.Environmentwhereengine.Environmentexists;modeIndexwraps@intFromEnumfor two callers;Modeis private whileAction,nextModeandprevModeare public and name it;loadAnsIntoProgrammerbounds its integer test with the literals-9223372036854775808.0and18446744073709551616.0; andhandleKeyis 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 Zigextern structfront ends (FR-3.13). Zig's own@offsetOfwould 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 splitsysv64, since it is 16 bytes on x86-64 and 8 on AArch64 macOS),_Atomic(which raises alignment on i386), and a barealigned(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 ccis 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.
sqrtdrawn as a radical in the calculator's expression, rather than assqrt((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.