diff --git a/.kiro/specs/calculator/requirements.md b/.kiro/specs/calculator/requirements.md index 8001755..183f7c3 100644 --- a/.kiro/specs/calculator/requirements.md +++ b/.kiro/specs/calculator/requirements.md @@ -16,7 +16,7 @@ A calculator application with three frontends (CLI, TUI, Android) sharing a comm - **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`. An argument outside a function's domain is reported as such, distinct from an unknown name: `asin(2)`, `ln(0)`, `log2(0)` and `factorial(-1)` report a domain error, `sqrt(-1)` reports "square root of a negative number", and only an unrecognized name reports an unknown function. - **FR-1.5**: Support constants: `pi`, `e`, `tau`. These are fixed values, readable and not assignable. - **FR-1.6**: Support variable storage: any identifier as a named variable. (The original wording restricted this to `A-F, X, Y, Z`; the implementation accepts any name, which is a superset and the better behaviour, so the requirement follows the code.) A constant is not an assignment target: `pi = 3` reports an error rather than accepting the assignment and ignoring it. -- **FR-1.6.1**: The last answer is a **pseudovariable**, not a constant: `Ans` (also `ans`) reads the result of the previous evaluation, the engine updates it on every successful evaluation, and the user cannot assign to it. It is a distinct concept from FR-1.5's constants, which never change, and from FR-1.6's variables, which only the user writes. See tasks.md open item 16: the engine currently folds `Ans` in with the constants and reports `AssignmentToConstant` ("cannot assign to a built-in constant") for `Ans = 5`, which misnames it. +- **FR-1.6.1**: The last answer is a **pseudovariable**, not a constant: `Ans` (also `ans`) reads the result of the previous evaluation, the engine updates it on every successful evaluation, and the user cannot assign to it. It is a distinct concept from FR-1.5's constants, which never change, and from FR-1.6's variables, which only the user writes. The set is exactly one name: the immediately preceding answer. Earlier results are FR-1.7's history, reachable by replay rather than by name. See tasks.md open item 16: the engine currently folds `Ans` in with the constants and reports `AssignmentToConstant` ("cannot assign to a built-in constant") for `Ans = 5`, which misnames it. - **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 * 2` is 2000, `1,234,567` is one number). A comma followed by any other number of digits is an argument separator, which is what makes `log(100,10)` two arguments rather than the number 10010. The ambiguous case `max(1,234)` resolves in favour of the grouping and reads as `max(1234)`; write a space to mean two arguments. A malformed group such as `1,00` is 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`, octal `0o`, or binary `0b`) 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. diff --git a/.kiro/specs/calculator/tasks.md b/.kiro/specs/calculator/tasks.md index 7c9ede5..3850567 100644 --- a/.kiro/specs/calculator/tasks.md +++ b/.kiro/specs/calculator/tasks.md @@ -1431,8 +1431,7 @@ STILL OPEN, in the order I would take them: silently discarded, but it did so by folding `Ans` in with `pi`, `e` and `tau` behind one `Environment.isBuiltIn`, and the error it raises is `AssignmentToConstant`, phrased "cannot assign to a built-in constant". `Ans` is - not a constant: it changes on every evaluation. Three things follow from - separating the concepts, none of them done: + not a constant: it changes on every evaluation. Two things follow, neither done: - The name and the message. A pseudovariable that the engine writes and the user only reads wants its own error (or a shared one worded to cover both), so @@ -1440,10 +1439,10 @@ STILL OPEN, in the order I would take them: - The read path. `getVar` special-cases the two spellings inline, ahead of the variable map, next to the constant table. A pseudovariable is a third kind of name and reads as one only if it is declared as one. - - What else belongs in the set. If the last answer is a pseudovariable, earlier - answers are the obvious next question, and FR-1.7's history is the thing that - already holds them. Nothing has been decided about naming or depth, and this - item is not a commitment to any of it. + + Scope is the immediately preceding answer and nothing else. No `Ans2`, no indexed + history: earlier results stay FR-1.7's replay, so the set is one name and the + third kind of name has exactly one member. --- diff --git a/engine/src/programmer.zig b/engine/src/programmer.zig index c895cfc..71b0b8b 100644 --- a/engine/src/programmer.zig +++ b/engine/src/programmer.zig @@ -51,14 +51,13 @@ pub const Config = struct { /// Evaluate an AST in programmer mode, producing an exact integer result. /// -/// The allocator is for parsing decimal literals, which go through the exact tier -/// so that `3.99` truncates the value the user wrote rather than an f64's -/// approximation of it. Nothing else here allocates. -pub fn evalProgrammer(allocator: Allocator, config: Config, expr: *const Expr) Error!Integer { - return evalExpr(allocator, config, expr); -} - -/// Recursively evaluate an expression. +/// The allocator is for parsing decimal literals, which go through the exact tier so +/// that `3.99` truncates the value the user wrote rather than an f64's approximation +/// of it. Nothing else here allocates. +/// +/// There used to be a `pub fn evalProgrammer` in front of this that forwarded to it +/// and nothing else. Its only caller was `evalProgrammerString` below, in this file, +/// so the parser-facing entry point was public API with no consumer. fn evalExpr(allocator: Allocator, config: Config, expr: *const Expr) Error!Integer { switch (expr.*) { .number => |n| return literalToInteger(allocator, config, n), @@ -175,7 +174,7 @@ pub fn evalProgrammerString(allocator: Allocator, source: []const u8, config: Co // Same ownership rule as evalStringInfo: the tree is ours to release, and the // returned Integer does not borrow from it. defer Parser.freeExpr(allocator, expr); - return evalProgrammer(allocator, config, expr); + return evalExpr(allocator, config, expr); } // -- Tests --