31 KiB
31 KiB
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:
Ansfor the last result, plus any identifier as a named variable. (The original wording restricted this toA-F, X, Y, Z; the implementation accepts any name, which is a superset and the better behaviour, so the requirement follows the code.) Assignment to a constant name (pi,e,tau,Ans) is currently accepted and then ignored, which is a known defect rather than intended behaviour. - FR-1.7: Maintain calculation history with replay capability.
- FR-1.8: Commas accepted as digit separators in input, but only in thousands groups: a comma must be followed by exactly three digits (
1,000 * 2is 2000,1,234,567is one number). A comma followed by any other number of digits is an argument separator, which is what makeslog(100,10)two arguments rather than the number 10010. The ambiguous casemax(1,234)resolves in favour of the grouping and reads asmax(1234); write a space to mean two arguments. A malformed group such as1,00is an error rather than a silently merged number. Spaces and underscores group hex/octal/binary literals (0xFF FF,0xFF_FF); commas do not. - FR-1.9: When a standard-mode expression contains any non-decimal literal (hex
0x, octal0o, or binary0b) and the result is a non-negative integer, enrich the result display with hex/octal/binary representations inline (without leaving standard mode). Uses the smallest standard bit width (8/16/32/64/128) that holds the value. This does not change the evaluation semantics (still f64 arithmetic,^is still power); it only augments the display. Fractional or negative results show decimal only.
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, 128-bit.
- FR-2.4: Support bitwise operators: AND (
&orand), OR (|oror), XOR (xorkeyword), NOT (~ornot), left shift (<<), right shift (logical>>>), arithmetic right shift (>>), rotate left (rol), rotate right (ror). Note:^is always exponentiation (never XOR) - see FR-2.12. - FR-2.12: The
^operator means exponentiation in all modes (never XOR). This avoids mode-dependent operator overloading. XOR is available only via thexorkeyword. Power is also available via**. This keeps every operator's meaning identical across standard and programmer modes. - 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-2.8: Configurable endianness display (big-endian default, little-endian available). Affects byte order in HEX and ASCII rows and byte-level visualizations. Does not change the underlying value - purely a display/interpretation toggle. Big-endian is the default so the HEX row reads as the number itself, matching the DEC, OCT and BIN rows; the little-endian view (x86 memory order) is one keystroke away. (This requirement previously specified a little-endian default, which the implementation deliberately did not follow.)
- FR-2.9: ASCII/text literal input via single-quoted strings:
'hello'packs ASCII bytes into the integer value. First character occupies the most significant used byte (big-endian packing when display is BE) or least significant byte (when display is LE). Useful for examining magic numbers, file signatures, protocol headers. - FR-2.10: ASCII interpretation display - when the current value contains printable ASCII bytes, show the text representation alongside the numeric bases (e.g.,
ASCII: "ELF."orASCII: ..lfwith dots for non-printable bytes). - FR-2.11: IEEE 754 floating-point interpretation (float.exposed style):
- Support float32 (single) and float64 (double) formats, selectable
- Decompose bit pattern into sign bit, exponent field, and significand/mantissa field with visual color-coding in the bit grid
- Display the evaluation formula: (-1)^sign * 2^(exponent - bias) * 1.mantissa
- Display the exact decimal value represented by the bit pattern
- Display delta to next and previous representable float (ULP - unit in last place)
- Handle special values: +/-infinity, NaN, denormals/subnormals, +/-zero
- Allow toggling individual bits and seeing the float value update in real-time
- Allow entering a decimal float value and seeing the closest IEEE 754 representation
- Show when a value is not exactly representable (rounding occurred)
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|in <to_unit>(e.g.,100 km to mi,32F in C). IMPLEMENTED as ordinary expression input, so no subcommand is needed:tally 32F in Cworks, as does typing it in the TUI standard mode. The value may itself be an expression (2*3 kg to lb). - FR-4.2: Support unit conversions within expressions:
5 kg + 3 lbevaluates in the left-hand unit. NOT YET IMPLEMENTED (needs unit-aware tokenizing and evaluation, unlike FR-4.1 which only needs thetokeyword as a trigger). - FR-4.3: Length: nm, um, mm, cm, m, km, in, ft, yd, mi, nmi (nautical mile), ly, au, pc. Base unit: m.
- FR-4.4: Mass/Weight: mg, g, kg, t (tonne), oz, lb, st (stone), ton (US short), lt (long ton). Base unit: kg.
- FR-4.5: Temperature: C (Celsius), F (Fahrenheit), K (Kelvin), R (Rankine). Base unit: C. These are the only affine conversions (they need an offset, not just a factor). Fahrenheit and Rankine scale by exactly 5/9, which has no terminating decimal form.
- FR-4.6: Time: ns, us, ms, s, min, h, d, wk, yr. Base unit: s. A year is the Julian year (365.25 days), the usual unit-conversion convention.
- FR-4.7: Digital storage: bit, B, and both decimal (kB, MB, GB, TB, PB) and binary (KiB, MiB, GiB, TiB, PiB) prefixes, kept distinct. Base unit: B.
- FR-4.8: Speed: m/s, km/h, mph, ft/s, kn (knots), c (speed of light). Base unit: m/s.
- FR-4.9: Area: mm2, cm2, m2, km2, in2, ft2, yd2, mi2, ha, acre. Base unit: m2.
- FR-4.10: Volume: mL, L, m3, cm3, in3, ft3, gal, qt, pt, cup, floz, tbsp, tsp, bbl. Base unit: L.
- FR-4.11: Energy: J, kJ, cal, kcal, Wh, kWh, BTU, eV, erg. Base unit: J.
- FR-4.12: Pressure: Pa, kPa, MPa, bar, mbar, atm, psi, torr, mmHg, inHg. Base unit: Pa.
- FR-4.13: Data rate: bps, kbps, Mbps, Gbps, Tbps, Bps, kBps, MBps, GBps. Base unit: bps.
- FR-4.14: Angle: rad, mrad, deg, grad, turn, arcmin, arcsec. Base unit: rad.
- FR-4.15: In CLI, unit conversion is plain expression input (FR-4.1):
tally 100 km to mi,tally 32F to C. Aconvertsubcommand is also accepted for explicitness and scripting (tally convert 100 km mi, with or without theto), but it is a convenience, not the only route. - FR-4.16: In TUI, a dedicated convert mode reachable from the mode tabs.
- FR-4.17: Unit system extensible - adding a unit or category requires only a table entry, no parser changes.
- FR-4.18: Unit names resolve by canonical name or alias (long forms and plurals). An exact case-sensitive match is tried first so case-distinguished units keep their meaning (
Kis Kelvin,Bis byte,kBis notKiB); a case-insensitive pass runs only as a fallback, so forgiving input likeKMorCelsiusstill works. - FR-4.19: Converting between units of different categories is an error (
IncompatibleUnits); an unrecognized unit name is an error (UnknownUnit). - FR-4.20: Conversion detection must not change the meaning of ordinary expressions. Only a standalone whitespace-delimited
toorintriggers it, and a unit name is only recognized at a token boundary (so the trailingsofsmootsis not read as seconds, and theininsideminis not read as a separator). Without a separator,eremains Euler's number rather than becoming a unit. - FR-4.21: Because
inis both a separator and the canonical name for inches, candidate separators are tried last-to-first and the first reading that yields a valid conversion wins. This must resolve all of:5 in in cm(5 inches to cm),100 mm in in(mm to inches),1 in in in(identity), and60 min in h(minutes to hours). Theconvertsubcommand must accept the same separators as bare expressions. - FR-4.22: Conversions must be exact wherever the units allow it. Conversion factors are declared as exact text (decimal or
p/q) and the f64 forms derived from them, so12 in to ftis exactly1,98.6 Fis exactly37 C, and3.6 km/his exactly1 m/s. Units defined in terms of pi (the angle units, and the parsec) have no exact rational factor and fall back to the inexact tier; a result reports whether its conversion was lossless.
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.2.1: The compound-interest relationship is solvable for any of its four variables: present value, future value, nominal annual rate, or years. The compounding frequency is an input rather than a variable, because a solution for it only exists in the narrow band between annual and continuous compounding and a fractional answer would be meaningless.
- FR-5.2.2: A solved rate is a NOMINAL annual rate. When the compounding frequency exceeds 1 the effective annual rate (APY) must be reported alongside it, since a nominal rate compared against a quoted effective rate is a wrong comparison. At annual compounding the two coincide and only one figure is shown.
- 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-5.6: Produce an amortization schedule for a loan: given principal, rate per period, and number of periods (with an optional payment override), report each period's payment, interest, principal, and remaining balance, plus totals. Figures round to whole cents by default, with the final period absorbing the rounding residue so the balance reaches exactly zero. A payment larger than the level payment retires the loan early; a payment that covers the interest but not the full term ends in a balloon payment; a payment that does not cover the first period's interest is an error rather than an infinite schedule.
- FR-5.7: Financial calculations must be reachable from a standard-mode expression as functions, not only from a dedicated mode, the same way unit conversion is reachable from a bare expression. Results compose with ordinary arithmetic, so
cagr(10000, 25000, 5) * 100is a valid expression. The functions are:cagr(start, end, periods),fv(pv, rate, years[, per_year]),pv(fv, rate, years[, per_year]),compound_rate(pv, fv, years[, per_year]),compound_years(pv, fv, rate[, per_year]),apy(nominal_rate, per_year),tvm_pmt/tvm_fv/tvm_pv/tvm_n/tvm_rate(each taking the other four variables),amort_payment(principal, rate, periods),amort_interest/amort_principal/amort_balance(..., period), andamort_total_interest/amort_total_paid. Anything the financial forms can solve must also be reachable this way, so the two surfaces do not drift apart. - FR-5.8: Financial results are in the inexact tier (FR-4.22's exactness model does not apply): every formula needs a non-integer power or a logarithm, so there is no exact rational to preserve. Money is handled by explicit rounding at the point a figure becomes an amount, banker's rounding (half-even) by default so repeated rounding does not bias totals upward.
FR-6: CLI Frontend
- FR-6.1: Invoke as
tally "<expression>"for standard mode evaluation. - FR-6.2: Flag
--programmeror-pfor 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 tvmwith 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:
--jsonfor machine-readable output, plain text default. - FR-6.10: Exit code 0 on success, non-zero on parse/evaluation errors with stderr message.
- FR-6.11: Subcommand
tally amort <principal> <rate> <periods> [payment]prints an amortization schedule as a table. Flags:--monthly(the rate given is an annual nominal rate charged over monthly periods, so rate/12 applies per period),--summary(totals only, no per-period rows),--exact(skip cent rounding). A table is the one financial output that cannot be an expression function, which is why it is a subcommand.
FR-7: TUI Frontend
- FR-7.1: Full-screen terminal UI with mode tabs (Standard, Programmer, Financial, Convert). Tab moves to the next mode and Shift-Tab to the previous one, so a mis-hit costs one keystroke rather than a full lap.
- FR-7.2: Standard mode: expression input line, result display, scrollable history.
- FR-7.3: Programmer mode: bit grid (navigable with arrow keys, toggled with Space), simultaneous base displays, expression input. NOTE: Enter does not toggle a bit. It is consumed by the input line, and the value-zone status hint that advertised
Enter:setis a known defect. Space is the only toggle. - FR-7.3.1: When the terminal is too short for the bit grid plus all six base rows, the view says so and how many rows it needs, rather than drawing rows over the separator and the input line.
- 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, and a scrollable amortization schedule. Four calculations behind one selector: CAGR, compound interest, TVM, and amortization. TVM and compound interest solve for whichever field is left blank; an unanswerable form reports what it is waiting for rather than doing nothing. Results are live, so financial mode records nothing in history.
- FR-7.5.1: Form fields group digits as they are typed: entering
200then0displays2,000. The stored text stays ungrouped so it still parses, and a typed comma is accepted and discarded rather than corrupting the value. - FR-7.5.2: Form fields accept expressions, not only numbers.
12 * 30in a period count is valid input, shown verbatim while it is an expression and evaluated in place by Enter. A field holding an unevaluated expression is reported as such, so it is never mistaken for a blank field the form is waiting to solve for. A field that does not evaluate keeps its text rather than being silently cleared. - FR-7.5.3: Rate fields display a trailing
%after the value, and accept a typed%as a no-op, so6%and6mean the same thing. The label alone is not sufficient affordance once the eye is on the value column. - FR-7.5.4: Where a default is near-universal, the form prefills it as a visible value rather than showing an empty field with a placeholder. Compounds/year starts at
1. This keeps "blank means solve for this" true for every field the user sees, instead of blank meaning a default in one place and a solve slot in another. Clearing a prefilled non-solvable field asks for a value rather than for an answer. - FR-7.6: Convert mode: select category, input value, select from/to units, live result.
- FR-7.6.1: The help overlay scrolls (Up/Down, PgUp/PgDn, or the mouse wheel) and shows its position when content is off screen. Any other key returns. Sections are a single line list rather than individually height-gated draw calls, which is what previously caused later sections to be dropped silently on a normal-sized terminal.
- FR-7.7: Every action must be reachable from the keyboard alone; the TUI is fully usable without a mouse.
- FR-7.8: REMOVED. Quick-switch keys (
d=dec,h=hex,o=oct,b=bin) to highlight the primary base cannot coexist with editable value fields: in the value zone those letters are hex and binary digits. The requirement predates editable fields, and the fields are the more useful feature. Base selection stays on the arrow keys and the mouse. - FR-7.9: Support terminal resize gracefully.
- FR-7.10: Vi-style and Emacs-style keybinding options for expression input. NOT IMPLEMENTED: the prompt is a plain text field. Deferred, not dropped.
FR-7.11: Mouse Support
Mouse input is a first-class way to drive the TUI, not an optional extra. Anything visibly interactive must respond to a left click. Keyboard parity is still required (FR-7.7): the mouse never becomes the only way to do something.
- FR-7.11.1: Clicking a mode tab switches to that mode.
- FR-7.11.2: Clicking a bit in the programmer bit grid flips that bit and moves the cursor to it.
- FR-7.11.3: Clicking a digit in the HEX or OCT row focuses that field and places the cursor on the exact digit clicked, ready for typing. A click does not alter the value, because a hex nibble or octal digit has no single unambiguous "toggle".
- FR-7.11.4: Clicking a digit in the BIN row flips that bit, since one binary digit is exactly one bit.
- FR-7.11.5: Clicking a DEC(s) or DEC(u) row focuses that field for editing.
- FR-7.11.6: Clicking the
Bits:,Signed:, orEndian:label in the programmer header toggles or cycles that setting. - FR-7.11.7: In the float view, clicking a bit flips it; clicking the format or exit hint in the header toggles f32/f64 or leaves the overlay.
- FR-7.11.8: In convert mode, clicking a category chip selects that category (resetting the unit pair to that category's defaults); clicking a unit in the From or To column selects it.
- FR-7.11.9: Clicking the input line returns keyboard focus to the expression prompt.
- FR-7.11.10: Clicking anywhere dismisses the help overlay, matching its "any key dismisses" keyboard behavior.
- FR-7.11.11: Only a left button press acts. Release, motion, and drag events are ignored so a single click cannot fire an action twice. The wheel is the one exception: in financial mode it scrolls the amortization schedule (FR-7.11.13).
- FR-7.11.12: Clickable regions are rebuilt every frame from what was actually drawn, so hit targets can never drift out of sync with the display.
- FR-7.11.13: In financial mode, clicking a calculation chip selects that calculation and clicking a field row focuses it for editing; the mouse wheel scrolls the amortization schedule. In the help overlay the wheel scrolls and a click returns.
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 buildinvocation 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
paholeor 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 is measured, not waived. The CLI and TUI each get their own instrumented coverage report alongside the engine's, with include paths kept disjoint so no file is accounted for twice. The TUI is testable further than "smoke tests" suggests: views are drawn into a surface and the cells read back, and key and mouse handling goes through the real handlers, so layout and binding regressions are caught rather than eyeballed. Target is the same >= 80% as the engine.
- What genuinely cannot be covered is the terminal itself: the event loop,
vaxissetup, and the process entry point. Those are the only frontend paths exempt. - 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 buildinvocation 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.tomlat the project root. A developer needs onlymiseinstalled - runningmise installprovisions 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.tomlis 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 inandroid/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). - Inexact (f64) values: scientific notation only when absolute value > 10^15 / < 10^-15. Never jump to scientific notation for values that fit in a readable decimal. This bound is not a readability preference: 10^15 is where f64 stops distinguishing consecutive integers (2^53 ~ 9.007 x 10^15), so printing a plain integer past it would assert precision the value does not have.
- (An earlier draft also triggered scientific notation past 15 significant digits. That clause was never implemented and was wrong: read literally it renders
0.9999999999999998as9.999999999999998e-1, which is worse.)
- (An earlier draft also triggered scientific notation past 15 significant digits. That clause was never implemented and was wrong: read literally it renders
- Exact values: the 10^15 bound above must NOT apply. It exists because of f64's precision cliff, and an exact value has no such cliff, so applying it would contradict NFR-9.1 -
9007199254740993is ~9.007 x 10^15 and would render as9.007199254740993e15, which is precisely the bug NFR-9.1 forbids. Exact values instead have a readability cap on integer digits (formatter.max_display_integer_digits), above which the display abbreviates to scientific notation while the clipboard/rawform retains every digit.- The cap must exceed the values the exact tier exists to serve:
9007199254740993(16 digits) and2^128(39 digits). It exists at all because without any cap,factorial(171)renders 310 digits and1.5e300 * 10renders 301: accurate but unreadable. - PROVISIONALLY 40 digits, pending review. Chosen as the smallest round number above
2^128. Not derived from any measured preference.
- The cap must exceed the values the exact tier exists to serve:
- Programmer mode hex values display with space-separated bytes (e.g.,
FF FF FF FF). - 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;
--rawflag or--jsongives 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).
NFR-9: Numeric Correctness (Exact Arithmetic)
Correctness is prioritized over evaluation speed. A calculator evaluates one expression at a time in response to a human, so there is no hot-loop or real-time constraint that justifies accepting wrong answers.
- NFR-9.1: Integer arithmetic must be exact and unbounded in standard mode.
9007199254740993must round-trip, and2^53 + 1must not collapse to2^53. Silently returning a different integer than the user typed is a correctness bug, not a display artifact. - NFR-9.2: Arithmetic on terminating decimal literals must be exact:
0.1 + 0.2is0.3, and1.1 + 2.2is3.3. - NFR-9.3: Division must be exact where the result is rational:
1/3retains its exact value and(1/3) * 3is exactly1. - NFR-9.4: Unit conversions between units whose factors are exact decimals must be exact:
12 in to ftis exactly1. - NFR-9.5: Exactness is tracked, not assumed. A value is exact only if no rounding occurred anywhere in its computation. Any operation involving an inexact operand yields an inexact result, and inexact values never silently become exact again.
- NFR-9.6: Operations that cannot produce a rational result (trigonometric, logarithmic, exponential, non-integer powers, roots of non-perfect-powers, and the constants
pi/e/tau) fall back to floating point. This is a per-evaluation property:sqrt(4)stays exact,sqrt(2)does not. - NFR-9.7: Input base must not affect the numeric path.
0xdeadbeef + 10is exact integer arithmetic; base is a lexical and display concern only. - NFR-9.8: Exact arithmetic must degrade gracefully rather than exhaust memory. Denominator size is capped, and exceeding the cap demotes the value to inexact rather than erroring.
- NFR-9.9: Display precision is a separate decision from compute precision. Exact values with terminating decimal expansions print exactly; non-terminating ones are rendered to a digit budget with correct rounding and marked approximate, with the exact fraction available.
- NFR-9.10: Programmer mode retains fixed-width
u128wrapping semantics, and the IEEE 754 float view retains binaryf32/f64semantics. Neither is affected by exact arithmetic. - NFR-9.11: The inexact tier must not claim more precision than its operations can deliver. Displayed digits must be digits we can justify, so the storage width is chosen to match the accuracy of the available transcendental implementations rather than exceeding it (see design.md 2.7.8).