human review: programmer.zig

This commit is contained in:
Emil Lerch 2026-08-21 12:38:54 -07:00
parent c6505570e3
commit bc225066dc
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 14 additions and 16 deletions

View file

@ -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.

View file

@ -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.
---

View file

@ -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 --