more structural changes from human review
This commit is contained in:
parent
cb7da0fe9e
commit
41d0e7cd11
16 changed files with 1451 additions and 1341 deletions
|
|
@ -61,10 +61,9 @@ build.zig (workspace root)
|
|||
|
||||
| Module | Responsibility |
|
||||
|--------|---------------|
|
||||
| `errors.zig` | `CalcError` and the one table of error wording |
|
||||
| `Integer.zig` | A fixed-width integer (file-as-struct), plus `IntType`, `BitWidth`, `Signedness` |
|
||||
| `rational.zig` | Exact rationals over big integers |
|
||||
| `number.zig` | The exact/inexact numeric model (section 2.7) |
|
||||
| `Rational.zig` | Exact rationals over big integers (file-as-struct) |
|
||||
| `number.zig` | The exact/inexact numeric model (section 2.7). Lowercase: `Number` is a tagged union, which a file-as-struct cannot express |
|
||||
| `tokenizer.zig` | Lexer, and `Base` for literals |
|
||||
| `ast.zig` | AST node definitions |
|
||||
| `parser.zig` | Pratt parser -> AST |
|
||||
|
|
@ -75,14 +74,25 @@ build.zig (workspace root)
|
|||
| `float_interp.zig` | IEEE 754 bit-level interpretation |
|
||||
| `units.zig` | Unit conversion tables and resolver |
|
||||
| `financial.zig` | CAGR, TVM, compound interest, amortization |
|
||||
| `engine.zig` | Public API surface (Zig-native). Imports and re-exports only; defines nothing |
|
||||
| `message.zig` | Gone: the wording lives in `engine.zig` beside the `Error` union it words |
|
||||
| `engine.zig` | Public API surface (Zig-native): the module re-exports, the `Error` union, and `phrase` |
|
||||
| `c_api.zig` | `extern "C"` wrappers for JNI/FFI consumers |
|
||||
|
||||
There is deliberately no `types.zig`. It existed, and being named after a language
|
||||
feature rather than a concept, it accumulated four unrelated groups: the error
|
||||
vocabulary, the fixed-width integer model, `Base` (used only by the lexer and the
|
||||
AST), and `Mode` (which the engine stored and never read). Each has gone to the
|
||||
module that owns it. `struct_layout.zig` is still unimplemented (Phase 4).
|
||||
Every module declares its own error set; there is no shared one (section 10).
|
||||
|
||||
There is deliberately no `types.zig` and no `errors.zig`. `types.zig` existed, and
|
||||
being named after a language feature rather than a concept, it accumulated four
|
||||
unrelated groups: the error vocabulary, the fixed-width integer model, `Base` (used
|
||||
only by the lexer and the AST), and `Mode` (which the engine stored and never read).
|
||||
`errors.zig` was where the error vocabulary landed, until the same question showed that
|
||||
what needed splitting was the type inside it. Each piece has gone to the module that
|
||||
owns it. `struct_layout.zig` is still unimplemented (Phase 4).
|
||||
|
||||
A file is named TitleCase when the file *is* the type, with its fields at container
|
||||
level: `Integer.zig` and `Rational.zig`. `number.zig` stays lowercase because `Number`
|
||||
is a tagged union and a Zig file is always a struct, so the file cannot be that type
|
||||
without wrapping the union in a field, which would add a hop at every use of the tag
|
||||
that is the type's whole identity.
|
||||
|
||||
### 2.2 Core Data Types
|
||||
|
||||
|
|
@ -1969,58 +1979,74 @@ The JNI bridge sends expression strings down and receives JSON results back. Thi
|
|||
|
||||
## 10. Error Handling Strategy
|
||||
|
||||
Each module declares what it can fail with, and the sets compose:
|
||||
|
||||
```zig
|
||||
pub const CalcError = error{
|
||||
// Parser errors
|
||||
UnexpectedToken,
|
||||
UnmatchedParen,
|
||||
InvalidNumber,
|
||||
UnknownFunction,
|
||||
UnknownVariable,
|
||||
|
||||
// Evaluation errors
|
||||
DivisionByZero,
|
||||
Overflow,
|
||||
InvalidOperandType,
|
||||
DomainError, // e.g., sqrt(-1)
|
||||
|
||||
// Struct layout errors
|
||||
InvalidType,
|
||||
InvalidFieldName,
|
||||
DuplicateFieldName,
|
||||
StructTooLarge,
|
||||
|
||||
// Financial errors
|
||||
InsufficientParameters,
|
||||
ConvergenceFailure, // TVM Newton-Raphson didn't converge
|
||||
|
||||
// System
|
||||
OutOfMemory,
|
||||
// parser.zig
|
||||
pub const Error = error{
|
||||
UnexpectedToken, UnmatchedParen, UnexpectedEnd,
|
||||
InvalidExpression, InvalidNumber, OutOfMemory,
|
||||
};
|
||||
|
||||
pub const ErrorInfo = struct {
|
||||
err: CalcError,
|
||||
message: []const u8,
|
||||
position: ?usize, // character position in input where error occurred
|
||||
context: []const u8, // snippet of input around error
|
||||
// Rational.zig, inherited by number.zig
|
||||
pub const Error = error{
|
||||
OutOfMemory, DivisionByZero, InvalidNumber,
|
||||
ExponentTooLarge, NegativeRoot,
|
||||
};
|
||||
|
||||
// bitwise.zig
|
||||
pub const Error = error{DomainError};
|
||||
|
||||
// units.zig: its own two, plus whatever the exact path raises
|
||||
pub const Error = error{ UnknownUnit, IncompatibleUnits, OutOfMemory } || number.Error;
|
||||
|
||||
// financial.zig
|
||||
pub const Error = error{
|
||||
InsufficientParameters, ConvergenceFailure,
|
||||
DomainError, DivisionByZero, OutOfMemory,
|
||||
};
|
||||
|
||||
// evaluator.zig: its own, plus every dependency's
|
||||
pub const Error = error{ UnknownFunction, UnknownVariable, DomainError, Overflow } ||
|
||||
parser.Error || number.Error || bitwise.Error || financial.Error;
|
||||
|
||||
// engine.zig: what any entry point can return, for frontends to switch over
|
||||
pub const Error = evaluator.Error || programmer.Error || units.Error || financial.Error;
|
||||
```
|
||||
|
||||
NOT IMPLEMENTED, and removed: nothing ever constructed an `ErrorInfo`, and the
|
||||
parser's `error_pos`/`had_error` fields that would have fed it were written on every
|
||||
error path and never read. Adding position reporting means threading it through the
|
||||
`CalcError` returns, which is worth doing deliberately rather than leaving a
|
||||
half-built shape in the code.
|
||||
This replaced one hand-written `CalcError` with 20 members that every engine function
|
||||
claimed to return. That signature was false in both directions: `parser.parse` said it
|
||||
might return `ConvergenceFailure` and `UnknownUnit`, so no caller could switch on what
|
||||
a parse can actually produce, and four members (`InvalidType`, `InvalidFieldName`,
|
||||
`DuplicateFieldName`, `StructTooLarge`) belonged to a struct layout module that does
|
||||
not exist, so nothing could return them while `errorPhrase` still gave them wording.
|
||||
|
||||
The phrase for each error lives in exactly one place, `types.errorPhrase`, whose
|
||||
switch has no `else`, so a new member of `CalcError` fails to compile until it is
|
||||
given a phrase. Frontends decorate that phrase at comptime (`switch (err) { inline
|
||||
else => ... }`): the CLI adds `error: ` and a newline, the TUI adds `error: `, and a
|
||||
view with better context can override individual cases, as the financial form does
|
||||
for `DomainError`. Each frontend used to carry its own copy of the whole table, and
|
||||
the TUI's had drifted three errors behind.
|
||||
Error members unify by name in Zig, so the per-module sets compose with no
|
||||
coordination: `parser.Error.OutOfMemory` and `units.Error.OutOfMemory` are the same
|
||||
value. The unions are written with `||` rather than enumerated, so the compiler
|
||||
maintains them.
|
||||
|
||||
All engine functions return `CalcError!Result`. Frontends translate these into user-facing messages.
|
||||
Deleting the single set also deleted the translation between tiers. `number.toCalcError`
|
||||
existed to map the numeric model's errors into the engine-wide set, and what it mapped
|
||||
away were the more useful names: `ExponentTooLarge` became `Overflow` and `NegativeRoot`
|
||||
became `DomainError`. Now `sqrt(-1)` reports "square root of a negative number" and
|
||||
`2^3000000` reports "the exponent is too large to compute".
|
||||
|
||||
Errors carry no position or context. An `ErrorInfo` with a source position was
|
||||
specified here and declared in the code, but nothing ever constructed one, and the
|
||||
parser fields that would have fed it were written on every error path and never read.
|
||||
Adding position reporting means threading it through the returns, which is worth doing
|
||||
deliberately rather than leaving a half-built shape in place.
|
||||
|
||||
The wording lives in exactly one place, `engine.phrase`, in the engine rather than a
|
||||
frontend because every frontend needs the same words, including the Android app across
|
||||
the C ABI. Its switch has no `else`, so an error added to any module's set fails to
|
||||
compile until it is given a phrase, and a comptime block checks the other direction, so
|
||||
it cannot carry wording for an error the engine is incapable of producing. Frontends
|
||||
decorate at comptime (`switch (err) { inline else => ... }`): the CLI adds `error: ` and
|
||||
a newline, the TUI adds `error: `, and a view with better context can override
|
||||
individual cases, as the financial form does for `DomainError`. Each frontend used to
|
||||
carry its own copy of the whole table, and the TUI's had drifted three errors behind.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ A calculator application with three frontends (CLI, TUI, Android) sharing a comm
|
|||
- **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`. An argument outside a function's domain is a domain error, distinct from an unknown name: `sqrt(-1)`, `asin(2)`, `ln(0)`, `log2(0)` and `factorial(-1)` all report a domain error, and only an unrecognized name reports an unknown function.
|
||||
- **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`.
|
||||
- **FR-1.6**: Support variable storage: `Ans` for the last result, plus 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.) 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.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ NOTE: Actual structure diverged from spec - single binary at `src/main.zig`
|
|||
(CLI + TUI combined), engine as static lib + shared lib. No separate cli/ or
|
||||
tui/ build files. kcov-based coverage wired in via `build/Coverage.zig`.
|
||||
|
||||
### Task 1.2: Implement core types module [DONE]
|
||||
### Task 1.2: Implement core types module [DONE, later dismantled]
|
||||
- Create `engine/src/types.zig`
|
||||
- Define `Value` union, `Integer` struct, `BitWidth`, `Signedness`, `Endianness` enums
|
||||
- Define `MultiBaseResult` struct
|
||||
|
|
@ -31,6 +31,12 @@ tui/ build files. kcov-based coverage wired in via `build/Coverage.zig`.
|
|||
- Define `Mode` enum (standard, programmer, financial)
|
||||
- Verify: compiles, types are importable from other engine modules
|
||||
|
||||
SUPERSEDED by Tasks 5.12 and 5.13. `types.zig` no longer exists: a module named after
|
||||
a language feature collected unrelated things. `Value`, `ErrorInfo` and `Mode` are gone
|
||||
entirely (unused, never constructed, and stored-but-never-read respectively), `Integer`
|
||||
and its enums are `Integer.zig`, `Endianness` is `std.builtin.Endian`, and `CalcError`
|
||||
is replaced by a per-module error set in each module that can fail.
|
||||
|
||||
### Task 1.3: Implement tokenizer [DONE]
|
||||
- Create `engine/src/tokenizer.zig`
|
||||
- Token types: numbers (dec, hex `0x`, oct `0o`, bin `0b`), operators, parens, identifiers, comma, semicolon, EOF
|
||||
|
|
@ -147,7 +153,9 @@ behavioral change (rationale in design.md 2.7.9), plus 2.0e for the unit factors
|
|||
- Added to `rational.zig`: `floor`, `ceil`, `round`, `mod` (the
|
||||
`a - b*floor(a/b)` definition), and unbounded exact `factorial`.
|
||||
- Added to `number.zig`: the matching wrappers plus `max`/`min`.
|
||||
- `rational.Error.ExponentTooLarge` maps to `CalcError.Overflow`.
|
||||
- `rational.Error.ExponentTooLarge` reached the caller as `CalcError.Overflow`. Both
|
||||
the mapping and the shared set are gone as of Task 5.13: the name now reaches the
|
||||
user, as "the exponent is too large to compute".
|
||||
- ALL 489 pre-existing tests pass unchanged. 522 total now (+33).
|
||||
- Verified through the unchanged f64 API: `0.1 + 0.2` = `0.3`, `1.1 + 2.2` = `3.3`,
|
||||
`0.1 * 3` = `0.3`, `0.1+0.2+0.3` = `0.6`, `(0.1+0.2)*10-3` = `0`,
|
||||
|
|
@ -756,6 +764,62 @@ Remaining subcommands deferred until their engine modules exist.
|
|||
- NOT DONE: mouse wheel scrolling for history
|
||||
- Verify: help overlay works, mouse interactions work, looks reasonable in 80x24 terminal
|
||||
|
||||
### Task 5.13: One error set per module, not one for the engine
|
||||
|
||||
Found by asking the same question of `errors.zig` that killed `types.zig`. The file
|
||||
was a defensible home; the type inside it was not. `CalcError` had 20 members and
|
||||
every engine function returned it, so `parser.parse` claimed it might return
|
||||
`ConvergenceFailure`, `UnknownUnit` and `StructTooLarge`, and no caller could switch
|
||||
on what a parse actually produces.
|
||||
|
||||
- Each module now declares what it can fail with: `parser.Error`, `bitwise.Error`,
|
||||
`units.Error`, `financial.Error`, `evaluator.Error`, `programmer.Error`, with
|
||||
`rational.Error` (inherited by `number.zig`) already in place.
|
||||
- The unions are `||` expressions, not lists: `evaluator.Error` is its own four members
|
||||
plus its dependencies', and `message.Error` is the union of the entry points. The
|
||||
compiler maintains them, so adding an error to one module propagates with no edit.
|
||||
- Verified against the compiler before relying on it: error members unify by name
|
||||
across sets, a set can be derived from a function's return type via `@typeInfo`, and
|
||||
recursion with an inferred error set resolves (so `evalExact` needed no annotation).
|
||||
- `number.toCalcError` is gone, and with it `units.mapNumberError` and
|
||||
`evaluator.mapError`, which were aliases of it. Those call sites are plain `try`.
|
||||
- Four members went away rather than staying unused: `InvalidType`,
|
||||
`InvalidFieldName`, `DuplicateFieldName` and `StructTooLarge` belonged to the struct
|
||||
layout module Phase 4 has not written, and `errorPhrase` was giving all four wording
|
||||
nothing could produce.
|
||||
- `errors.zig` is gone. The wording lives in `engine.zig`, next to the `Error` union
|
||||
it words, and it stays in the engine rather than a frontend because the Android app
|
||||
will receive these strings across the C ABI. It briefly lived in a `message.zig` of
|
||||
its own; that file was folded in during review, since nothing in the engine calls it
|
||||
and a separate file for one table did not pay for itself. `phrase`'s switch has no
|
||||
`else` (a new error fails to compile until worded), and it cannot word an error the
|
||||
engine is incapable of returning, because a prong naming one is a type error against
|
||||
`Error`. That second property is the direction the hand-written set got wrong, and it
|
||||
needs no assertion: a comptime block asserting it was written, found to be redundant
|
||||
when tested against the compiler, and removed.
|
||||
|
||||
Two user-visible improvements fell out, because the translation being deleted was
|
||||
flattening the better names: `sqrt(-1)` now says "square root of a negative number"
|
||||
instead of "domain error", and `2^3000000` says "the exponent is too large to compute"
|
||||
instead of "overflow". Two tests updated to match.
|
||||
|
||||
- Verify: 944 tests pass, fmt and zlint clean, CLI checked across parse, name,
|
||||
arithmetic, unit and financial errors. `engine.zig` at 95.6% (the two uncovered lines
|
||||
are a diagnostic branch inside a passing test).
|
||||
|
||||
### Task 5.14: File-as-struct for the types that are types
|
||||
|
||||
`Integer.zig` and `Rational.zig` are named TitleCase and the file *is* the type: the
|
||||
fields sit at container level, `@This()` names it, and the auxiliary declarations
|
||||
(`IntType`, `BitWidth`, `Signedness`; `Error`, `DecimalResult`) are nested inside.
|
||||
`git mv` kept the history.
|
||||
|
||||
`number.zig` stays lowercase. `Number` is a tagged union and a Zig file is always a
|
||||
struct container, so the file can only be that type by wrapping the union in a field.
|
||||
That would put a `.value` hop in front of 53 tag tests, and the exact/inexact tag is
|
||||
the type's whole identity, so the wrapper would cost more than the naming consistency
|
||||
buys. Recorded here so the asymmetry reads as a decision rather than an oversight.
|
||||
|
||||
### Task 5.12: Break up types.zig
|
||||
|
||||
`types.zig` was named after a language feature rather than a concept, so it
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -29,7 +29,10 @@ const Integer = @import("Integer.zig");
|
|||
const BitWidth = Integer.BitWidth;
|
||||
const Signedness = Integer.Signedness;
|
||||
const IntType = Integer.IntType;
|
||||
const CalcError = @import("errors.zig").CalcError;
|
||||
|
||||
/// The one way a fixed-width operation can fail: a shift or rotate distance that is
|
||||
/// negative in the operand's type. Everything else about these operators is total.
|
||||
pub const Error = error{DomainError};
|
||||
|
||||
/// The operators this module implements.
|
||||
///
|
||||
|
|
@ -79,8 +82,8 @@ const Distance = union(enum) {
|
|||
///
|
||||
/// A negative distance is a domain error rather than a very large one. Standard
|
||||
/// mode used to reduce it modulo 64, so `8 >> -1` quietly became `8 >> 63`.
|
||||
fn distance(int_type: IntType, right: u128) CalcError!Distance {
|
||||
if (int_type.isNegative(right)) return CalcError.DomainError;
|
||||
fn distance(int_type: IntType, right: u128) Error!Distance {
|
||||
if (int_type.isNegative(right)) return Error.DomainError;
|
||||
const value = right & int_type.mask();
|
||||
if (value >= int_type.bits()) return .past_width;
|
||||
return .{ .within = @intCast(value) };
|
||||
|
|
@ -90,13 +93,13 @@ fn distance(int_type: IntType, right: u128) CalcError!Distance {
|
|||
///
|
||||
/// Rotation is cyclic, so a distance beyond the width is reduced rather than
|
||||
/// saturated: rotating a 64-bit value by 65 is rotating it by 1.
|
||||
fn rotation(int_type: IntType, right: u128) CalcError!u7 {
|
||||
if (int_type.isNegative(right)) return CalcError.DomainError;
|
||||
fn rotation(int_type: IntType, right: u128) Error!u7 {
|
||||
if (int_type.isNegative(right)) return Error.DomainError;
|
||||
return @intCast((right & int_type.mask()) % int_type.bits());
|
||||
}
|
||||
|
||||
/// Apply a fixed-width operation. Operands and result are masked bit patterns.
|
||||
pub fn apply(int_type: IntType, op: Op, left_in: u128, right_in: u128) CalcError!u128 {
|
||||
pub fn apply(int_type: IntType, op: Op, left_in: u128, right_in: u128) Error!u128 {
|
||||
const mask = int_type.mask();
|
||||
const left = left_in & mask;
|
||||
const right = right_in & mask;
|
||||
|
|
@ -234,11 +237,11 @@ test "shifting by one less than the width still keeps a bit" {
|
|||
|
||||
test "a negative shift distance is a domain error, not a huge one" {
|
||||
const neg_one: u128 = 0xFF; // -1 in 8-bit signed
|
||||
try testing.expectError(CalcError.DomainError, apply(i8_type, .shift_left, 1, neg_one));
|
||||
try testing.expectError(CalcError.DomainError, apply(i8_type, .shift_right, 1, neg_one));
|
||||
try testing.expectError(CalcError.DomainError, apply(i8_type, .shift_right_logical, 1, neg_one));
|
||||
try testing.expectError(CalcError.DomainError, apply(i8_type, .rotate_left, 1, neg_one));
|
||||
try testing.expectError(CalcError.DomainError, apply(i8_type, .rotate_right, 1, neg_one));
|
||||
try testing.expectError(Error.DomainError, apply(i8_type, .shift_left, 1, neg_one));
|
||||
try testing.expectError(Error.DomainError, apply(i8_type, .shift_right, 1, neg_one));
|
||||
try testing.expectError(Error.DomainError, apply(i8_type, .shift_right_logical, 1, neg_one));
|
||||
try testing.expectError(Error.DomainError, apply(i8_type, .rotate_left, 1, neg_one));
|
||||
try testing.expectError(Error.DomainError, apply(i8_type, .rotate_right, 1, neg_one));
|
||||
// The same pattern in an unsigned domain is 255, a distance past the width.
|
||||
try testing.expectEqual(@as(u128, 0), try apply(u8_type, .shift_left, 1, neg_one));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,13 @@
|
|||
//!
|
||||
//! Pure computation library with no I/O. Provides expression parsing, evaluation,
|
||||
//! programmer-mode bit manipulation, unit conversion, and financial calculations.
|
||||
//!
|
||||
//! This file is a facade: it imports and re-exports, and defines nothing. Anything
|
||||
//! defined here would have to be imported back by the modules below, which is how a
|
||||
//! root file becomes a dependency of its own leaves.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// Vocabulary, lowest first.
|
||||
pub const errors = @import("errors.zig");
|
||||
pub const Integer = @import("Integer.zig");
|
||||
// Exact numeric model (design.md 2.7). The evaluator computes in these.
|
||||
pub const rational = @import("rational.zig");
|
||||
pub const Rational = @import("Rational.zig");
|
||||
pub const number = @import("number.zig");
|
||||
// Language layer.
|
||||
pub const tokenizer = @import("tokenizer.zig");
|
||||
|
|
@ -33,7 +30,6 @@ pub const financial = @import("financial.zig");
|
|||
// curated re-export of nearly every public declaration, which drifted: two thirds
|
||||
// of it had no callers, and `Value` was re-exported after the type it named had
|
||||
// stopped being the engine's result type.
|
||||
pub const CalcError = errors.CalcError;
|
||||
pub const BitWidth = Integer.BitWidth;
|
||||
pub const Environment = evaluator.Environment;
|
||||
pub const evalString = evaluator.evalString;
|
||||
|
|
@ -44,8 +40,140 @@ pub const UnitCategory = units.UnitCategory;
|
|||
pub const UnitDef = units.UnitDef;
|
||||
pub const Number = number.Number;
|
||||
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
/// Every error an engine entry point can return.
|
||||
///
|
||||
/// Derived, not enumerated: each module declares what it can fail with
|
||||
/// (`parser.Error`, `units.Error`, `financial.Error`, and so on), and adding an error
|
||||
/// to any of those adds it here with no list to keep in step. There used to be one
|
||||
/// hand-written `CalcError` with 20 members that every engine function claimed to
|
||||
/// return, four of which nothing could produce.
|
||||
pub const Error = evaluator.Error ||
|
||||
programmer.Error ||
|
||||
units.Error ||
|
||||
financial.Error;
|
||||
|
||||
/// The human-readable phrase for an error, with no prefix and no newline.
|
||||
///
|
||||
/// One table for every frontend, because they all need the same words: the CLI and
|
||||
/// the TUI call this, and the Android app will receive these strings across the C
|
||||
/// ABI. Each frontend adds its own decoration ("error: " and a newline for the CLI,
|
||||
/// "error: " for the TUI) and a view with better context can override individual
|
||||
/// cases, as the financial form does.
|
||||
///
|
||||
/// The switch has no `else`, so an error added to any module's set fails to compile
|
||||
/// here rather than falling back to something vague. It also cannot word an error the
|
||||
/// engine is incapable of returning: a prong naming one is a type error, since the
|
||||
/// switch is over `Error`. That second property is what the hand-written `CalcError`
|
||||
/// got wrong, and it needs no assertion of its own to hold. It carried `InvalidType`,
|
||||
/// `InvalidFieldName`, `DuplicateFieldName` and `StructTooLarge` for a struct layout
|
||||
/// module that does not exist yet, and gave all four a phrase.
|
||||
pub fn phrase(err: Error) []const u8 {
|
||||
return switch (err) {
|
||||
// Parsing
|
||||
error.UnexpectedToken => "unexpected token",
|
||||
error.UnmatchedParen => "unmatched parenthesis",
|
||||
error.UnexpectedEnd => "unexpected end of expression",
|
||||
error.InvalidExpression => "invalid expression",
|
||||
error.InvalidNumber => "invalid number",
|
||||
|
||||
// Names
|
||||
error.UnknownFunction => "unknown function",
|
||||
error.UnknownVariable => "unknown variable",
|
||||
|
||||
// Arithmetic
|
||||
error.DivisionByZero => "division by zero",
|
||||
error.DomainError => "domain error",
|
||||
error.Overflow => "overflow",
|
||||
error.InvalidOperandType => "invalid operand type",
|
||||
// These two used to be flattened into Overflow and DomainError by a mapping
|
||||
// between error sets. The specific wording is the whole reason the numeric
|
||||
// tier bothered to distinguish them.
|
||||
error.ExponentTooLarge => "the exponent is too large to compute",
|
||||
error.NegativeRoot => "square root of a negative number",
|
||||
|
||||
// Units
|
||||
error.UnknownUnit => "unknown unit",
|
||||
error.IncompatibleUnits => "incompatible units (different categories)",
|
||||
|
||||
// Financial
|
||||
error.InsufficientParameters => "these values do not determine an answer",
|
||||
error.ConvergenceFailure => "no solution found",
|
||||
|
||||
// System
|
||||
error.OutOfMemory => "out of memory",
|
||||
};
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
// -- Tests --
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test {
|
||||
testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
test "every error the engine can return has its own phrase" {
|
||||
// Exhaustive by construction; this checks the qualities the switch cannot state:
|
||||
// non-empty, undecorated, single-line, and mutually distinct.
|
||||
const fields = @typeInfo(Error).error_set.?;
|
||||
var seen: [fields.len][]const u8 = undefined;
|
||||
inline for (fields, 0..) |field, i| {
|
||||
const text = phrase(@field(Error, field.name));
|
||||
try testing.expect(text.len > 0);
|
||||
// No prefix and no newline: decoration belongs to the caller.
|
||||
try testing.expect(!std.mem.startsWith(u8, text, "error"));
|
||||
try testing.expect(std.mem.indexOfScalar(u8, text, '\n') == null);
|
||||
seen[i] = text;
|
||||
}
|
||||
|
||||
for (seen, 0..) |text, i| {
|
||||
for (seen[i + 1 ..]) |other| {
|
||||
if (std.mem.eql(u8, text, other)) {
|
||||
std.debug.print("two errors share the phrase \"{s}\"\n", .{text});
|
||||
return error.TestUnexpectedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "the error set is derived from the modules, not enumerated here" {
|
||||
// A module's errors reach `Error` without this file naming them, which is what
|
||||
// makes the per-module sets safe to extend.
|
||||
inline for (@typeInfo(financial.Error).error_set.?) |field| {
|
||||
const promoted: Error = @field(Error, field.name);
|
||||
try testing.expect(phrase(promoted).len > 0);
|
||||
}
|
||||
inline for (@typeInfo(units.Error).error_set.?) |field| {
|
||||
const promoted: Error = @field(Error, field.name);
|
||||
try testing.expect(phrase(promoted).len > 0);
|
||||
}
|
||||
inline for (@typeInfo(parser.Error).error_set.?) |field| {
|
||||
const promoted: Error = @field(Error, field.name);
|
||||
try testing.expect(phrase(promoted).len > 0);
|
||||
}
|
||||
}
|
||||
|
||||
test "the struct-layout errors are gone, not merely unused" {
|
||||
// They were members of the old set with phrases nothing could produce. Absence
|
||||
// is the assertion: naming one in `phrase` would not compile.
|
||||
inline for (@typeInfo(Error).error_set.?) |field| {
|
||||
try testing.expect(!std.mem.eql(u8, field.name, "StructTooLarge"));
|
||||
try testing.expect(!std.mem.eql(u8, field.name, "InvalidFieldName"));
|
||||
try testing.expect(!std.mem.eql(u8, field.name, "DuplicateFieldName"));
|
||||
try testing.expect(!std.mem.eql(u8, field.name, "InvalidType"));
|
||||
}
|
||||
}
|
||||
|
||||
test "phrase is usable at comptime, which is how frontends decorate it" {
|
||||
const decorated = comptime "error: " ++ phrase(error.DivisionByZero);
|
||||
try testing.expectEqualStrings("error: division by zero", decorated);
|
||||
}
|
||||
|
||||
test "the cases the TUI table used to lose" {
|
||||
try testing.expectEqualStrings("invalid expression", phrase(error.InvalidExpression));
|
||||
try testing.expectEqualStrings("no solution found", phrase(error.ConvergenceFailure));
|
||||
try testing.expectEqualStrings(
|
||||
"these values do not determine an answer",
|
||||
phrase(error.InsufficientParameters),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
//! The engine's error vocabulary, and the one place its wording lives.
|
||||
//!
|
||||
//! Every engine function returns `CalcError!T`. Errors carry no position or
|
||||
//! context: an `ErrorInfo` with a source position was once specified and declared,
|
||||
//! but nothing ever constructed one, and the parser fields that would have fed it
|
||||
//! were written and never read. Adding position reporting means threading it
|
||||
//! through the returns, which is a change to make deliberately.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// All possible engine errors.
|
||||
pub const CalcError = error{
|
||||
// Parser errors
|
||||
UnexpectedToken,
|
||||
UnmatchedParen,
|
||||
InvalidNumber,
|
||||
UnknownFunction,
|
||||
UnknownVariable,
|
||||
UnexpectedEnd,
|
||||
InvalidExpression,
|
||||
|
||||
// Evaluation errors
|
||||
DivisionByZero,
|
||||
Overflow,
|
||||
InvalidOperandType,
|
||||
DomainError,
|
||||
|
||||
// Struct layout errors
|
||||
InvalidType,
|
||||
InvalidFieldName,
|
||||
DuplicateFieldName,
|
||||
StructTooLarge,
|
||||
|
||||
// Financial errors
|
||||
InsufficientParameters,
|
||||
ConvergenceFailure,
|
||||
|
||||
// Unit conversion errors
|
||||
UnknownUnit,
|
||||
IncompatibleUnits,
|
||||
|
||||
// System
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
/// The human-readable phrase for an error, with no prefix and no newline.
|
||||
///
|
||||
/// The single source of these strings. The CLI and the TUI each had their own
|
||||
/// switch over the same error set, differing only in punctuation and in what they
|
||||
/// had forgotten: the TUI was missing `InsufficientParameters`, `ConvergenceFailure`
|
||||
/// and `InvalidExpression` and rendered all three as "evaluation error". Callers add
|
||||
/// their own decoration ("error: " and a newline for the CLI, "error: " for the
|
||||
/// TUI), and a view with better context can still override individual cases, as the
|
||||
/// financial form does.
|
||||
///
|
||||
/// The switch has no `else`, so an error added to the set is a compile error here
|
||||
/// rather than a silent fallback to a vague phrase.
|
||||
pub fn errorPhrase(err: CalcError) []const u8 {
|
||||
return switch (err) {
|
||||
// Parser
|
||||
CalcError.UnexpectedToken => "unexpected token",
|
||||
CalcError.UnmatchedParen => "unmatched parenthesis",
|
||||
CalcError.InvalidNumber => "invalid number",
|
||||
CalcError.UnknownFunction => "unknown function",
|
||||
CalcError.UnknownVariable => "unknown variable",
|
||||
CalcError.UnexpectedEnd => "unexpected end of expression",
|
||||
CalcError.InvalidExpression => "invalid expression",
|
||||
|
||||
// Evaluation
|
||||
CalcError.DivisionByZero => "division by zero",
|
||||
CalcError.Overflow => "overflow",
|
||||
CalcError.InvalidOperandType => "invalid operand type",
|
||||
CalcError.DomainError => "domain error",
|
||||
|
||||
// Struct layout
|
||||
CalcError.InvalidType => "invalid type",
|
||||
CalcError.InvalidFieldName => "invalid field name",
|
||||
CalcError.DuplicateFieldName => "duplicate field name",
|
||||
CalcError.StructTooLarge => "struct too large",
|
||||
|
||||
// Financial
|
||||
CalcError.InsufficientParameters => "these values do not determine an answer",
|
||||
CalcError.ConvergenceFailure => "no solution found",
|
||||
|
||||
// Units
|
||||
CalcError.UnknownUnit => "unknown unit",
|
||||
CalcError.IncompatibleUnits => "incompatible units (different categories)",
|
||||
|
||||
// System
|
||||
CalcError.OutOfMemory => "out of memory",
|
||||
};
|
||||
}
|
||||
|
||||
// -- Tests --
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "errorPhrase: every error in the set has its own phrase" {
|
||||
// Exhaustive by construction: the switch in errorPhrase has no else branch, so
|
||||
// adding an error to CalcError without a phrase is a compile error rather than a
|
||||
// silent fallback. This walks the set to prove the phrases are distinct and
|
||||
// non-empty.
|
||||
const fields = @typeInfo(CalcError).error_set.?;
|
||||
var seen: [fields.len][]const u8 = undefined;
|
||||
inline for (fields, 0..) |field, i| {
|
||||
const phrase = errorPhrase(@field(CalcError, field.name));
|
||||
try testing.expect(phrase.len > 0);
|
||||
// No prefix and no newline: decoration belongs to the caller.
|
||||
try testing.expect(!std.mem.startsWith(u8, phrase, "error"));
|
||||
try testing.expect(std.mem.indexOfScalar(u8, phrase, '\n') == null);
|
||||
seen[i] = phrase;
|
||||
}
|
||||
|
||||
for (seen, 0..) |phrase, i| {
|
||||
for (seen[i + 1 ..]) |other| {
|
||||
if (std.mem.eql(u8, phrase, other)) {
|
||||
std.debug.print("two errors share the phrase \"{s}\"\n", .{phrase});
|
||||
return error.TestUnexpectedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "errorPhrase: usable at comptime, which is how frontends decorate it" {
|
||||
const decorated = comptime "error: " ++ errorPhrase(CalcError.DivisionByZero);
|
||||
try testing.expectEqualStrings("error: division by zero", decorated);
|
||||
}
|
||||
|
||||
test "errorPhrase: the cases the TUI table used to lose" {
|
||||
try testing.expectEqualStrings("invalid expression", errorPhrase(CalcError.InvalidExpression));
|
||||
try testing.expectEqualStrings("no solution found", errorPhrase(CalcError.ConvergenceFailure));
|
||||
try testing.expectEqualStrings(
|
||||
"these values do not determine an answer",
|
||||
errorPhrase(CalcError.InsufficientParameters),
|
||||
);
|
||||
}
|
||||
|
|
@ -13,7 +13,18 @@ const Expr = ast.Expr;
|
|||
const BinaryOp = ast.BinaryOp;
|
||||
const Integer = @import("Integer.zig");
|
||||
const IntType = Integer.IntType;
|
||||
const CalcError = @import("errors.zig").CalcError;
|
||||
/// What standard-mode evaluation can fail with.
|
||||
///
|
||||
/// Its own name and range errors, plus everything its dependencies can raise. The
|
||||
/// `||` chain is the honest signature: financial functions are callable from an
|
||||
/// expression, so `ConvergenceFailure` really can come out of `evalString`, while a
|
||||
/// bare `parser.parse` cannot produce it and no longer claims to.
|
||||
pub const Error = error{
|
||||
UnknownFunction,
|
||||
UnknownVariable,
|
||||
DomainError,
|
||||
Overflow,
|
||||
} || parser_mod.Error || number_mod.Error || bitwise.Error || financial.Error;
|
||||
const parser_mod = @import("parser.zig");
|
||||
const Parser = parser_mod.Parser;
|
||||
const number_mod = @import("number.zig");
|
||||
|
|
@ -122,7 +133,7 @@ pub const Environment = struct {
|
|||
///
|
||||
/// Task 2.0c replaces this boundary with a `Number`-returning API, which is what
|
||||
/// the remaining integer-precision cases need.
|
||||
pub fn evaluate(env: *Environment, expr: *const Expr) CalcError!f64 {
|
||||
pub fn evaluate(env: *Environment, expr: *const Expr) Error!f64 {
|
||||
// A scratch arena keeps Number lifetimes trivial: nothing in the recursive
|
||||
// evaluator has to free intermediates, and the caller's allocator is never
|
||||
// left holding them regardless of whether it is an arena itself.
|
||||
|
|
@ -136,35 +147,35 @@ pub fn evaluate(env: *Environment, expr: *const Expr) CalcError!f64 {
|
|||
|
||||
/// The exact evaluation core. Produces a `Number`, staying exact until an
|
||||
/// operation forces the float fallback (see design.md 2.7.4).
|
||||
fn evalExact(env: *Environment, scratch: Allocator, expr: *const Expr) CalcError!Number {
|
||||
fn evalExact(env: *Environment, scratch: Allocator, expr: *const Expr) Error!Number {
|
||||
switch (expr.*) {
|
||||
.number => |n| return literalToNumber(scratch, n),
|
||||
.string_literal => |text| {
|
||||
// Pack ASCII bytes into an integer (BE packing, as programmer mode).
|
||||
var packed_value: u128 = 0;
|
||||
for (text) |byte| {
|
||||
if (byte > 0x7F) return CalcError.InvalidNumber;
|
||||
if (byte > 0x7F) return Error.InvalidNumber;
|
||||
packed_value = (packed_value << 8) | byte;
|
||||
}
|
||||
return Number.fromInt(scratch, packed_value) catch |err| return mapError(err);
|
||||
return try Number.fromInt(scratch, packed_value);
|
||||
},
|
||||
.variable => |name| {
|
||||
// getVar hands back a borrowed value owned by the environment, so
|
||||
// copy it into the evaluation arena before it takes part in
|
||||
// arithmetic that the arena will later free.
|
||||
const value = env.getVar(name) orelse return CalcError.UnknownVariable;
|
||||
return value.cloneWith(scratch) catch |err| mapError(err);
|
||||
const value = env.getVar(name) orelse return Error.UnknownVariable;
|
||||
return try value.cloneWith(scratch);
|
||||
},
|
||||
.assignment => |a| {
|
||||
const val = try evalExact(env, scratch, a.value);
|
||||
// setVar copies, so storing an arena-allocated value is safe.
|
||||
env.setVar(a.name, val) catch return CalcError.OutOfMemory;
|
||||
env.setVar(a.name, val) catch return Error.OutOfMemory;
|
||||
return val;
|
||||
},
|
||||
.unary => |u| {
|
||||
const operand = try evalExact(env, scratch, u.operand);
|
||||
return switch (u.op) {
|
||||
.negate => Number.negate(scratch, operand) catch |err| mapError(err),
|
||||
.negate => try Number.negate(scratch, operand),
|
||||
// Bitwise NOT is a fixed-width integer operation, not rational
|
||||
// arithmetic, so it drops to the float/integer path. The width is
|
||||
// the fixed standard-mode one: this used to read
|
||||
|
|
@ -193,7 +204,7 @@ fn evalExact(env: *Environment, scratch: Allocator, expr: *const Expr) CalcError
|
|||
/// Decimal literals are re-parsed from their source text rather than taken from
|
||||
/// `float_value`, because `float_value` has already rounded: `0.1` cannot be
|
||||
/// recovered from its binary approximation.
|
||||
fn literalToNumber(scratch: Allocator, n: ast.Expr.Number) CalcError!Number {
|
||||
fn literalToNumber(scratch: Allocator, n: ast.Expr.Number) Error!Number {
|
||||
if (n.base == .decimal and n.text.len > 0) {
|
||||
if (Number.parse(scratch, n.text)) |value| return value else |_| {
|
||||
// Fall through to the approximations below rather than failing: the
|
||||
|
|
@ -204,25 +215,20 @@ fn literalToNumber(scratch: Allocator, n: ast.Expr.Number) CalcError!Number {
|
|||
// Non-decimal literals are integers; use the exact integer the tokenizer
|
||||
// recovered when it fits, otherwise accept the float approximation.
|
||||
if (n.int_value) |int_val| {
|
||||
return Number.fromInt(scratch, int_val) catch |err| return mapError(err);
|
||||
return try Number.fromInt(scratch, int_val);
|
||||
}
|
||||
return Number.fromFloat(n.float_value);
|
||||
}
|
||||
|
||||
/// Map the numeric model's errors onto the engine's error set.
|
||||
///
|
||||
/// One mapping, in `number.zig`; this alias keeps the call sites short.
|
||||
const mapError = number_mod.toCalcError;
|
||||
|
||||
/// Evaluate a binary operation.
|
||||
fn evalBinaryOp(scratch: Allocator, op: BinaryOp, left: Number, right: Number) CalcError!Number {
|
||||
fn evalBinaryOp(scratch: Allocator, op: BinaryOp, left: Number, right: Number) Error!Number {
|
||||
return switch (op) {
|
||||
.add => Number.add(scratch, left, right) catch |err| mapError(err),
|
||||
.sub => Number.sub(scratch, left, right) catch |err| mapError(err),
|
||||
.mul => Number.mul(scratch, left, right) catch |err| mapError(err),
|
||||
.div => Number.div(scratch, left, right) catch |err| mapError(err),
|
||||
.mod => Number.mod(scratch, left, right) catch |err| mapError(err),
|
||||
.pow => Number.pow(scratch, left, right) catch |err| mapError(err),
|
||||
.add => try Number.add(scratch, left, right),
|
||||
.sub => try Number.sub(scratch, left, right),
|
||||
.mul => try Number.mul(scratch, left, right),
|
||||
.div => try Number.div(scratch, left, right),
|
||||
.mod => try Number.mod(scratch, left, right),
|
||||
.pow => try Number.pow(scratch, left, right),
|
||||
// The remaining operators are fixed-width integer operations rather than
|
||||
// rational arithmetic, so they work on the 64-bit projection, in the shared
|
||||
// implementation programmer mode also uses (FR-2.12). `inline else`
|
||||
|
|
@ -257,12 +263,12 @@ const standard_int_type: IntType = .{};
|
|||
/// process: `2^64 and 1` and `~1e30` both killed it, and a NaN operand produced a
|
||||
/// garbage answer instead. An operand that does not fit the width is a reportable
|
||||
/// error, not a crash.
|
||||
fn toFixedWidthBits(value: f64) CalcError!u128 {
|
||||
if (!math.isFinite(value)) return CalcError.DomainError;
|
||||
fn toFixedWidthBits(value: f64) Error!u128 {
|
||||
if (!math.isFinite(value)) return Error.DomainError;
|
||||
// i64 covers [-2^63, 2^63); 2^63 itself is the first excluded value and is
|
||||
// exactly representable, so these bounds are exact.
|
||||
if (value >= 9223372036854775808.0 or value < -9223372036854775808.0) {
|
||||
return CalcError.Overflow;
|
||||
return Error.Overflow;
|
||||
}
|
||||
const bits: u64 = @bitCast(@as(i64, @intFromFloat(value)));
|
||||
return @as(u128, bits);
|
||||
|
|
@ -274,39 +280,40 @@ fn fromFixedWidthBits(bits: u128) Number {
|
|||
}
|
||||
|
||||
/// Evaluate a built-in function call.
|
||||
fn evalFunction(env: *Environment, scratch: Allocator, name: []const u8, args: []const *Expr) CalcError!Number {
|
||||
fn evalFunction(env: *Environment, scratch: Allocator, name: []const u8, args: []const *Expr) Error!Number {
|
||||
// Single-argument functions
|
||||
if (args.len == 1) {
|
||||
const x = try evalExact(env, scratch, args[0]);
|
||||
|
||||
// Functions with an exact implementation.
|
||||
if (std.mem.eql(u8, name, "abs")) {
|
||||
return Number.abs(scratch, x) catch |err| mapError(err);
|
||||
return try Number.abs(scratch, x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "floor")) {
|
||||
return Number.floor(scratch, x) catch |err| mapError(err);
|
||||
return try Number.floor(scratch, x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "ceil")) {
|
||||
return Number.ceil(scratch, x) catch |err| mapError(err);
|
||||
return try Number.ceil(scratch, x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "round")) {
|
||||
return Number.round(scratch, x) catch |err| mapError(err);
|
||||
return try Number.round(scratch, x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "sqrt")) {
|
||||
// The negative-input rule lives in Number.sqrt, which raises
|
||||
// NegativeRoot; mapError turns that into a domain error.
|
||||
return Number.sqrt(scratch, x) catch |err| mapError(err);
|
||||
// NegativeRoot. That name now reaches the user instead of being
|
||||
// flattened into "domain error".
|
||||
return try Number.sqrt(scratch, x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "factorial")) {
|
||||
const result = Number.factorial(scratch, x) catch |err| return mapError(err);
|
||||
const result = try Number.factorial(scratch, x);
|
||||
// Null means the argument was negative or fractional, which is a domain
|
||||
// error, not an unknown function.
|
||||
return result orelse CalcError.DomainError;
|
||||
return result orelse Error.DomainError;
|
||||
}
|
||||
|
||||
// Everything else escapes the rationals, so it falls back to f64.
|
||||
const f = try evalSingleArgFn(name, x.toFloat(scratch)) orelse
|
||||
return CalcError.UnknownFunction;
|
||||
return Error.UnknownFunction;
|
||||
return Number.fromFloat(f);
|
||||
}
|
||||
|
||||
|
|
@ -316,10 +323,10 @@ fn evalFunction(env: *Environment, scratch: Allocator, name: []const u8, args: [
|
|||
const b = try evalExact(env, scratch, args[1]);
|
||||
|
||||
if (std.mem.eql(u8, name, "max")) {
|
||||
return Number.max(scratch, a, b) catch |err| mapError(err);
|
||||
return try Number.max(scratch, a, b);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "min")) {
|
||||
return Number.min(scratch, a, b) catch |err| mapError(err);
|
||||
return try Number.min(scratch, a, b);
|
||||
}
|
||||
|
||||
const x = a.toFloat(scratch);
|
||||
|
|
@ -332,7 +339,7 @@ fn evalFunction(env: *Environment, scratch: Allocator, name: []const u8, args: [
|
|||
}
|
||||
if (std.mem.eql(u8, name, "log")) {
|
||||
// log(value, base)
|
||||
if (y <= 0 or y == 1 or x <= 0) return CalcError.DomainError;
|
||||
if (y <= 0 or y == 1 or x <= 0) return Error.DomainError;
|
||||
return Number.fromFloat(@log(x) / @log(y));
|
||||
}
|
||||
}
|
||||
|
|
@ -361,15 +368,15 @@ fn evalFunction(env: *Environment, scratch: Allocator, name: []const u8, args: [
|
|||
}
|
||||
}
|
||||
|
||||
return CalcError.UnknownFunction;
|
||||
return Error.UnknownFunction;
|
||||
}
|
||||
|
||||
/// A whole period count or 1-based period index, validated.
|
||||
fn periodCount(value: f64) CalcError!usize {
|
||||
if (!math.isFinite(value)) return CalcError.DomainError;
|
||||
if (@floor(value) != value) return CalcError.DomainError;
|
||||
fn periodCount(value: f64) Error!usize {
|
||||
if (!math.isFinite(value)) return Error.DomainError;
|
||||
if (@floor(value) != value) return Error.DomainError;
|
||||
if (value < 1 or value > @as(f64, @floatFromInt(financial.max_schedule_periods))) {
|
||||
return CalcError.DomainError;
|
||||
return Error.DomainError;
|
||||
}
|
||||
return @intFromFloat(value);
|
||||
}
|
||||
|
|
@ -383,7 +390,7 @@ fn periodCount(value: f64) CalcError!usize {
|
|||
///
|
||||
/// Returns null when `name` is not a financial function, so the caller can carry
|
||||
/// on to report an unknown function.
|
||||
fn evalFinancialFn(name: []const u8, a: []const f64) CalcError!?f64 {
|
||||
fn evalFinancialFn(name: []const u8, a: []const f64) Error!?f64 {
|
||||
if (a.len == 3) {
|
||||
// cagr(start, end, periods) -> growth rate as a fraction.
|
||||
if (std.mem.eql(u8, name, "cagr")) return try financial.cagr(a[0], a[1], a[2]);
|
||||
|
|
@ -478,16 +485,16 @@ fn evalFinancialFn(name: []const u8, a: []const f64) CalcError!?f64 {
|
|||
/// Returns null when `name` is not one of these functions, and an error when the
|
||||
/// name is known but the argument is outside its domain. The two used to be the
|
||||
/// same answer (null), so the caller reported `asin(2)` as "unknown function".
|
||||
fn evalSingleArgFn(name: []const u8, x: f64) CalcError!?f64 {
|
||||
fn evalSingleArgFn(name: []const u8, x: f64) Error!?f64 {
|
||||
if (std.mem.eql(u8, name, "sin")) return @sin(x);
|
||||
if (std.mem.eql(u8, name, "cos")) return @cos(x);
|
||||
if (std.mem.eql(u8, name, "tan")) return @tan(x);
|
||||
if (std.mem.eql(u8, name, "asin")) {
|
||||
if (x < -1 or x > 1) return CalcError.DomainError;
|
||||
if (x < -1 or x > 1) return Error.DomainError;
|
||||
return math.asin(x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "acos")) {
|
||||
if (x < -1 or x > 1) return CalcError.DomainError;
|
||||
if (x < -1 or x > 1) return Error.DomainError;
|
||||
return math.acos(x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "atan")) return math.atan(x);
|
||||
|
|
@ -495,15 +502,15 @@ fn evalSingleArgFn(name: []const u8, x: f64) CalcError!?f64 {
|
|||
// log already reported this as a domain error; the one-argument forms returned
|
||||
// -inf or NaN.
|
||||
if (std.mem.eql(u8, name, "log") or std.mem.eql(u8, name, "log10")) {
|
||||
if (x <= 0) return CalcError.DomainError;
|
||||
if (x <= 0) return Error.DomainError;
|
||||
return @log10(x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "ln")) {
|
||||
if (x <= 0) return CalcError.DomainError;
|
||||
if (x <= 0) return Error.DomainError;
|
||||
return @log(x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "log2")) {
|
||||
if (x <= 0) return CalcError.DomainError;
|
||||
if (x <= 0) return Error.DomainError;
|
||||
return @log2(x);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "cbrt")) return math.cbrt(x);
|
||||
|
|
@ -522,14 +529,14 @@ pub const EvalInfo = struct {
|
|||
|
||||
/// High-level evaluate: parse a string and evaluate it.
|
||||
/// Updates env.ans on success. The caller owns the returned value.
|
||||
pub fn evalString(env: *Environment, allocator: Allocator, source: []const u8) CalcError!Number {
|
||||
pub fn evalString(env: *Environment, allocator: Allocator, source: []const u8) Error!Number {
|
||||
const info = try evalStringInfo(env, allocator, source);
|
||||
return info.value;
|
||||
}
|
||||
|
||||
/// Like evalString but returns metadata (whether the expression used
|
||||
/// non-decimal literals) so frontends can decide to show a multi-base view.
|
||||
pub fn evalStringInfo(env: *Environment, allocator: Allocator, source: []const u8) CalcError!EvalInfo {
|
||||
pub fn evalStringInfo(env: *Environment, allocator: Allocator, source: []const u8) Error!EvalInfo {
|
||||
var p = Parser.init(allocator, source);
|
||||
const expr = try p.parse();
|
||||
// The parser hands over ownership. Nothing in the result borrows from the
|
||||
|
|
@ -546,9 +553,9 @@ pub fn evalStringInfo(env: *Environment, allocator: Allocator, source: []const u
|
|||
const scratch = arena.allocator();
|
||||
|
||||
const raw = try evalExact(env, scratch, expr);
|
||||
const result = raw.cloneWith(allocator) catch |err| return mapError(err);
|
||||
const result = try raw.cloneWith(allocator);
|
||||
|
||||
env.setAns(result) catch return CalcError.OutOfMemory;
|
||||
env.setAns(result) catch return Error.OutOfMemory;
|
||||
return .{
|
||||
.value = result,
|
||||
.has_nondecimal_literal = hasNonDecimalLiteral(expr),
|
||||
|
|
@ -629,7 +636,7 @@ test "eval division" {
|
|||
|
||||
test "eval division by zero" {
|
||||
const result = testEval("1 / 0");
|
||||
try testing.expectError(CalcError.DivisionByZero, result);
|
||||
try testing.expectError(Error.DivisionByZero, result);
|
||||
}
|
||||
|
||||
test "eval modulo" {
|
||||
|
|
@ -754,12 +761,12 @@ test "eval min" {
|
|||
|
||||
test "eval unknown function" {
|
||||
const result = testEval("bogus(1)");
|
||||
try testing.expectError(CalcError.UnknownFunction, result);
|
||||
try testing.expectError(Error.UnknownFunction, result);
|
||||
}
|
||||
|
||||
test "eval unknown variable" {
|
||||
const result = testEval("xyz");
|
||||
try testing.expectError(CalcError.UnknownVariable, result);
|
||||
try testing.expectError(Error.UnknownVariable, result);
|
||||
}
|
||||
|
||||
test "eval variable assignment and use" {
|
||||
|
|
@ -906,10 +913,10 @@ test "standard mode: a shift runs to completion instead of wrapping the distance
|
|||
|
||||
test "standard mode: a negative shift distance is a domain error" {
|
||||
// It used to be reduced modulo 64, so `8 >> -1` quietly became `8 >> 63`.
|
||||
try testing.expectError(CalcError.DomainError, testEval("8 >> 0 - 1"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("8 << 0 - 1"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("8 >>> 0 - 1"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("8 rol 0 - 1"));
|
||||
try testing.expectError(Error.DomainError, testEval("8 >> 0 - 1"));
|
||||
try testing.expectError(Error.DomainError, testEval("8 << 0 - 1"));
|
||||
try testing.expectError(Error.DomainError, testEval("8 >>> 0 - 1"));
|
||||
try testing.expectError(Error.DomainError, testEval("8 rol 0 - 1"));
|
||||
}
|
||||
|
||||
test "standard mode: rotation is cyclic, not clamped" {
|
||||
|
|
@ -965,30 +972,34 @@ test "eval log with base" {
|
|||
|
||||
test "eval log domain error" {
|
||||
const result = testEval("log(-1, 10)");
|
||||
try testing.expectError(CalcError.DomainError, result);
|
||||
try testing.expectError(Error.DomainError, result);
|
||||
}
|
||||
|
||||
test "domain errors are domain errors, not unknown functions" {
|
||||
// These pinned the wrong contract: the name is known, the argument is not in
|
||||
// its domain. Reporting "unknown function" sent the user looking for a typo.
|
||||
try testing.expectError(CalcError.DomainError, testEval("asin(2)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("asin(-2)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("acos(2)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("sqrt(-1)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("factorial(-1)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("factorial(2.5)"));
|
||||
try testing.expectError(Error.DomainError, testEval("asin(2)"));
|
||||
try testing.expectError(Error.DomainError, testEval("asin(-2)"));
|
||||
try testing.expectError(Error.DomainError, testEval("acos(2)"));
|
||||
try testing.expectError(Error.DomainError, testEval("factorial(-1)"));
|
||||
try testing.expectError(Error.DomainError, testEval("factorial(2.5)"));
|
||||
// Logarithms of non-positive values, which used to return -inf or NaN. The
|
||||
// two-argument form already reported this correctly.
|
||||
try testing.expectError(CalcError.DomainError, testEval("ln(0)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("ln(0 - 1)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("log(0)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("log10(0 - 5)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("log2(0)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("log(100, 1)"));
|
||||
try testing.expectError(Error.DomainError, testEval("ln(0)"));
|
||||
try testing.expectError(Error.DomainError, testEval("ln(0 - 1)"));
|
||||
try testing.expectError(Error.DomainError, testEval("log(0)"));
|
||||
try testing.expectError(Error.DomainError, testEval("log10(0 - 5)"));
|
||||
try testing.expectError(Error.DomainError, testEval("log2(0)"));
|
||||
try testing.expectError(Error.DomainError, testEval("log(100, 1)"));
|
||||
|
||||
// sqrt says which domain rule was broken, because the numeric tier raises its
|
||||
// own error and nothing flattens it on the way out.
|
||||
try testing.expectError(Error.NegativeRoot, testEval("sqrt(-1)"));
|
||||
try testing.expectError(Error.NegativeRoot, testEval("sqrt(0 - 4)"));
|
||||
|
||||
// A genuinely unknown name still reports one.
|
||||
try testing.expectError(CalcError.UnknownFunction, testEval("nope(1)"));
|
||||
try testing.expectError(CalcError.UnknownFunction, testEval("asin(1, 2)"));
|
||||
try testing.expectError(Error.UnknownFunction, testEval("nope(1)"));
|
||||
try testing.expectError(Error.UnknownFunction, testEval("asin(1, 2)"));
|
||||
}
|
||||
|
||||
test "the functions themselves still work inside their domains" {
|
||||
|
|
@ -1053,7 +1064,7 @@ test "eval string literal multi-char in standard mode" {
|
|||
test "eval string literal with non-ASCII byte errors" {
|
||||
// byte > 0x7F is rejected
|
||||
const result = testEval("'\x80'");
|
||||
try testing.expectError(CalcError.InvalidNumber, result);
|
||||
try testing.expectError(Error.InvalidNumber, result);
|
||||
}
|
||||
|
||||
test "eval rand zero-arg function returns 0" {
|
||||
|
|
@ -1063,17 +1074,17 @@ test "eval rand zero-arg function returns 0" {
|
|||
|
||||
test "eval unknown zero-arg function" {
|
||||
const result = testEval("bogus()");
|
||||
try testing.expectError(CalcError.UnknownFunction, result);
|
||||
try testing.expectError(Error.UnknownFunction, result);
|
||||
}
|
||||
|
||||
test "eval unknown three-arg function" {
|
||||
const result = testEval("bogus(1, 2, 3)");
|
||||
try testing.expectError(CalcError.UnknownFunction, result);
|
||||
try testing.expectError(Error.UnknownFunction, result);
|
||||
}
|
||||
|
||||
test "eval unknown two-arg function" {
|
||||
const result = testEval("bogus(1, 2)");
|
||||
try testing.expectError(CalcError.UnknownFunction, result);
|
||||
try testing.expectError(Error.UnknownFunction, result);
|
||||
}
|
||||
|
||||
// -- Exact arithmetic (Task 2.0b) --
|
||||
|
|
@ -1191,10 +1202,10 @@ test "exact: a transcendental contaminates the rest of the expression" {
|
|||
try testing.expectApproxEqAbs(@as(f64, 0.3), result, 1e-15);
|
||||
}
|
||||
|
||||
test "exact: overflow from an absurd exponent is reported as overflow" {
|
||||
// The exponent guard in the rational layer surfaces as Overflow rather than
|
||||
// silently producing infinity or exhausting memory.
|
||||
try testing.expectError(CalcError.Overflow, testEval("2 ^ 3000000"));
|
||||
test "exact: an absurd exponent is reported as an exponent that is too large" {
|
||||
// The rational layer's guard reaches the caller by its own name rather than as a
|
||||
// generic Overflow, which is what the old single error set turned it into.
|
||||
try testing.expectError(Error.ExponentTooLarge, testEval("2 ^ 3000000"));
|
||||
}
|
||||
|
||||
// -- Exactness visible through the Number API (Task 2.0c) --
|
||||
|
|
@ -1430,7 +1441,7 @@ test "financial: apy converts a nominal rate to an effective one" {
|
|||
try testEval("apy(compound_rate(1000, fv(1000, 18, 5, 12), 5, 12), 12)"),
|
||||
1e-4,
|
||||
);
|
||||
try testing.expectError(CalcError.DomainError, testEval("apy(5, 0)"));
|
||||
try testing.expectError(Error.DomainError, testEval("apy(5, 0)"));
|
||||
}
|
||||
|
||||
test "financial: the tvm solvers are reachable as four-argument functions" {
|
||||
|
|
@ -1475,26 +1486,26 @@ test "financial: results are inexact, so they do not claim exactness" {
|
|||
|
||||
test "financial: bad arguments are domain errors, not wrong answers" {
|
||||
// Zero periods.
|
||||
try testing.expectError(CalcError.DomainError, testEval("cagr(1000, 2000, 0)"));
|
||||
try testing.expectError(Error.DomainError, testEval("cagr(1000, 2000, 0)"));
|
||||
// A fractional period count cannot index an amortization schedule.
|
||||
try testing.expectError(CalcError.DomainError, testEval("amort_interest(200000, 0.5, 360.5, 1)"));
|
||||
try testing.expectError(Error.DomainError, testEval("amort_interest(200000, 0.5, 360.5, 1)"));
|
||||
// Period past the end of the schedule.
|
||||
try testing.expectError(CalcError.DomainError, testEval("amort_balance(200000, 0.5, 360, 361)"));
|
||||
try testing.expectError(Error.DomainError, testEval("amort_balance(200000, 0.5, 360, 361)"));
|
||||
// Payments that never retire the loan.
|
||||
try testing.expectError(CalcError.DomainError, testEval("amort_payment(0, 0.5, 360)"));
|
||||
try testing.expectError(Error.DomainError, testEval("amort_payment(0, 0.5, 360)"));
|
||||
// Period counts outside the schedule bounds.
|
||||
try testing.expectError(CalcError.DomainError, testEval("amort_payment(200000, 0.5, 0)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("amort_payment(200000, 0.5, 20000)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("amort_payment(200000, 0.5, 10^400)"));
|
||||
try testing.expectError(Error.DomainError, testEval("amort_payment(200000, 0.5, 0)"));
|
||||
try testing.expectError(Error.DomainError, testEval("amort_payment(200000, 0.5, 20000)"));
|
||||
try testing.expectError(Error.DomainError, testEval("amort_payment(200000, 0.5, 10^400)"));
|
||||
}
|
||||
|
||||
test "financial: wrong argument counts are unknown functions, not silent defaults" {
|
||||
try testing.expectError(CalcError.UnknownFunction, testEval("cagr(10000, 25000)"));
|
||||
try testing.expectError(CalcError.UnknownFunction, testEval("tvm_pmt(360, 0.5, 200000)"));
|
||||
try testing.expectError(CalcError.UnknownFunction, testEval("amort_payment(200000, 0.5, 360, 1)"));
|
||||
try testing.expectError(Error.UnknownFunction, testEval("cagr(10000, 25000)"));
|
||||
try testing.expectError(Error.UnknownFunction, testEval("tvm_pmt(360, 0.5, 200000)"));
|
||||
try testing.expectError(Error.UnknownFunction, testEval("amort_payment(200000, 0.5, 360, 1)"));
|
||||
// A three or four argument call to something that is not a function at all.
|
||||
try testing.expectError(CalcError.UnknownFunction, testEval("nope(1, 2, 3)"));
|
||||
try testing.expectError(CalcError.UnknownFunction, testEval("nope(1, 2, 3, 4)"));
|
||||
try testing.expectError(Error.UnknownFunction, testEval("nope(1, 2, 3)"));
|
||||
try testing.expectError(Error.UnknownFunction, testEval("nope(1, 2, 3, 4)"));
|
||||
}
|
||||
|
||||
// -- The AST is not the caller's problem --
|
||||
|
|
@ -1596,9 +1607,9 @@ test "grouped digits still work, inside and outside a call" {
|
|||
test "a malformed group is an error, not a silently merged number" {
|
||||
// Two digits after the comma is neither a group nor a valid argument list
|
||||
// here, so it fails loudly instead of evaluating as 100.
|
||||
try testing.expectError(CalcError.UnexpectedToken, testEval("1,00"));
|
||||
try testing.expectError(CalcError.UnexpectedToken, testEval("1,0000"));
|
||||
try testing.expectError(CalcError.UnexpectedToken, testEval("2+3,4"));
|
||||
try testing.expectError(Error.UnexpectedToken, testEval("1,00"));
|
||||
try testing.expectError(Error.UnexpectedToken, testEval("1,0000"));
|
||||
try testing.expectError(Error.UnexpectedToken, testEval("2+3,4"));
|
||||
}
|
||||
|
||||
test "a grouped literal past 2^53 is still exact" {
|
||||
|
|
@ -1638,16 +1649,16 @@ test "bitwise operands outside i64 report overflow instead of aborting" {
|
|||
};
|
||||
for (cases) |source| {
|
||||
const result = testEval(source);
|
||||
try testing.expectError(CalcError.Overflow, result);
|
||||
try testing.expectError(Error.Overflow, result);
|
||||
}
|
||||
}
|
||||
|
||||
test "a non-finite bitwise operand is a domain error" {
|
||||
// ln(-1) is NaN, and 1/0 raises before it can reach here, so NaN arrives via
|
||||
// the transcendental fallback.
|
||||
try testing.expectError(CalcError.DomainError, testEval("~ln(-1)"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("ln(-1) and 1"));
|
||||
try testing.expectError(CalcError.DomainError, testEval("1 << ln(-1)"));
|
||||
try testing.expectError(Error.DomainError, testEval("~ln(-1)"));
|
||||
try testing.expectError(Error.DomainError, testEval("ln(-1) and 1"));
|
||||
try testing.expectError(Error.DomainError, testEval("1 << ln(-1)"));
|
||||
}
|
||||
|
||||
test "bitwise operators still work at the edges of the range" {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,18 @@
|
|||
|
||||
const std = @import("std");
|
||||
const math = std.math;
|
||||
const CalcError = @import("errors.zig").CalcError;
|
||||
/// What the financial calculations can fail with.
|
||||
///
|
||||
/// `InsufficientParameters` and `ConvergenceFailure` are theirs alone: no other part
|
||||
/// of the engine can produce either, and under the old single error set every
|
||||
/// function in the engine claimed both.
|
||||
pub const Error = error{
|
||||
InsufficientParameters,
|
||||
ConvergenceFailure,
|
||||
DomainError,
|
||||
DivisionByZero,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
/// Iteration cap for the rate solver.
|
||||
pub const max_iterations: usize = 1000;
|
||||
|
|
@ -40,11 +51,11 @@ pub const tolerance: f64 = 1e-10;
|
|||
/// Compound annual growth rate, as a decimal fraction (0.2011 means 20.11%).
|
||||
///
|
||||
/// cagr = (end / start)^(1/periods) - 1
|
||||
pub fn cagr(start_value: f64, end_value: f64, periods: f64) CalcError!f64 {
|
||||
if (periods <= 0) return CalcError.DomainError;
|
||||
pub fn cagr(start_value: f64, end_value: f64, periods: f64) Error!f64 {
|
||||
if (periods <= 0) return Error.DomainError;
|
||||
// A zero or negative starting value has no meaningful growth rate, and a
|
||||
// negative ending value would need a complex root.
|
||||
if (start_value <= 0 or end_value < 0) return CalcError.DomainError;
|
||||
if (start_value <= 0 or end_value < 0) return Error.DomainError;
|
||||
return math.pow(f64, end_value / start_value, 1.0 / periods) - 1.0;
|
||||
}
|
||||
|
||||
|
|
@ -59,11 +70,11 @@ pub fn compoundFutureValue(
|
|||
annual_rate: f64,
|
||||
years: f64,
|
||||
compounds_per_year: f64,
|
||||
) CalcError!f64 {
|
||||
if (compounds_per_year <= 0) return CalcError.DomainError;
|
||||
if (years < 0) return CalcError.DomainError;
|
||||
) Error!f64 {
|
||||
if (compounds_per_year <= 0) return Error.DomainError;
|
||||
if (years < 0) return Error.DomainError;
|
||||
const periodic = annual_rate / 100.0 / compounds_per_year;
|
||||
if (periodic <= -1.0) return CalcError.DomainError;
|
||||
if (periodic <= -1.0) return Error.DomainError;
|
||||
return present_value * math.pow(f64, 1.0 + periodic, compounds_per_year * years);
|
||||
}
|
||||
|
||||
|
|
@ -74,13 +85,13 @@ pub fn compoundPresentValue(
|
|||
annual_rate: f64,
|
||||
years: f64,
|
||||
compounds_per_year: f64,
|
||||
) CalcError!f64 {
|
||||
if (compounds_per_year <= 0) return CalcError.DomainError;
|
||||
if (years < 0) return CalcError.DomainError;
|
||||
) Error!f64 {
|
||||
if (compounds_per_year <= 0) return Error.DomainError;
|
||||
if (years < 0) return Error.DomainError;
|
||||
const periodic = annual_rate / 100.0 / compounds_per_year;
|
||||
if (periodic <= -1.0) return CalcError.DomainError;
|
||||
if (periodic <= -1.0) return Error.DomainError;
|
||||
const factor = math.pow(f64, 1.0 + periodic, compounds_per_year * years);
|
||||
if (factor == 0) return CalcError.DivisionByZero;
|
||||
if (factor == 0) return Error.DivisionByZero;
|
||||
return future_value / factor;
|
||||
}
|
||||
|
||||
|
|
@ -101,21 +112,21 @@ pub fn compoundRate(
|
|||
future_value: f64,
|
||||
years: f64,
|
||||
compounds_per_year: f64,
|
||||
) CalcError!f64 {
|
||||
if (compounds_per_year <= 0) return CalcError.DomainError;
|
||||
) Error!f64 {
|
||||
if (compounds_per_year <= 0) return Error.DomainError;
|
||||
// With no time elapsed, any rate satisfies pv == fv and none satisfies
|
||||
// pv != fv, so there is no answer to give.
|
||||
if (years <= 0) return CalcError.DomainError;
|
||||
if (present_value == 0) return CalcError.DomainError;
|
||||
if (years <= 0) return Error.DomainError;
|
||||
if (present_value == 0) return Error.DomainError;
|
||||
|
||||
const ratio = future_value / present_value;
|
||||
// A sign change has no real root: no rate turns 1000 into -500.
|
||||
if (!(ratio > 0)) return CalcError.DomainError;
|
||||
if (!(ratio > 0)) return Error.DomainError;
|
||||
|
||||
const periods = compounds_per_year * years;
|
||||
const periodic = math.pow(f64, ratio, 1.0 / periods) - 1.0;
|
||||
const rate = periodic * compounds_per_year * 100.0;
|
||||
if (!math.isFinite(rate)) return CalcError.DomainError;
|
||||
if (!math.isFinite(rate)) return Error.DomainError;
|
||||
return rate;
|
||||
}
|
||||
|
||||
|
|
@ -128,23 +139,23 @@ pub fn compoundPeriods(
|
|||
future_value: f64,
|
||||
annual_rate: f64,
|
||||
compounds_per_year: f64,
|
||||
) CalcError!f64 {
|
||||
if (compounds_per_year <= 0) return CalcError.DomainError;
|
||||
if (present_value == 0) return CalcError.DomainError;
|
||||
) Error!f64 {
|
||||
if (compounds_per_year <= 0) return Error.DomainError;
|
||||
if (present_value == 0) return Error.DomainError;
|
||||
|
||||
const ratio = future_value / present_value;
|
||||
if (!(ratio > 0)) return CalcError.DomainError;
|
||||
if (!(ratio > 0)) return Error.DomainError;
|
||||
// Already there, whatever the rate.
|
||||
if (ratio == 1) return 0;
|
||||
|
||||
const periodic = annual_rate / 100.0 / compounds_per_year;
|
||||
if (periodic <= -1.0) return CalcError.DomainError;
|
||||
if (periodic <= -1.0) return Error.DomainError;
|
||||
// A zero rate never moves the balance, so no amount of time reaches a
|
||||
// different future value.
|
||||
if (periodic == 0) return CalcError.DomainError;
|
||||
if (periodic == 0) return Error.DomainError;
|
||||
|
||||
const years = @log(ratio) / @log(1.0 + periodic) / compounds_per_year;
|
||||
if (!math.isFinite(years)) return CalcError.DomainError;
|
||||
if (!math.isFinite(years)) return Error.DomainError;
|
||||
return years;
|
||||
}
|
||||
|
||||
|
|
@ -155,12 +166,12 @@ pub fn compoundPeriods(
|
|||
///
|
||||
/// 18% compounded monthly is 19.56% effective. Reporting a solved nominal rate
|
||||
/// without this is how rate comparisons go wrong.
|
||||
pub fn effectiveAnnualRate(annual_rate: f64, compounds_per_year: f64) CalcError!f64 {
|
||||
if (compounds_per_year <= 0) return CalcError.DomainError;
|
||||
pub fn effectiveAnnualRate(annual_rate: f64, compounds_per_year: f64) Error!f64 {
|
||||
if (compounds_per_year <= 0) return Error.DomainError;
|
||||
const periodic = annual_rate / 100.0 / compounds_per_year;
|
||||
if (periodic <= -1.0) return CalcError.DomainError;
|
||||
if (periodic <= -1.0) return Error.DomainError;
|
||||
const grown = math.pow(f64, 1.0 + periodic, compounds_per_year);
|
||||
if (!math.isFinite(grown)) return CalcError.DomainError;
|
||||
if (!math.isFinite(grown)) return Error.DomainError;
|
||||
return (grown - 1.0) * 100.0;
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +237,7 @@ fn tvmResidual(rate: f64, periods: f64, pv: f64, pmt: f64, fv: f64, due: bool) f
|
|||
}
|
||||
|
||||
/// Solve for whichever variable is null.
|
||||
pub fn solveTvm(params: TvmParams) CalcError!TvmSolution {
|
||||
pub fn solveTvm(params: TvmParams) Error!TvmSolution {
|
||||
// Exactly one unknown.
|
||||
var unknowns: usize = 0;
|
||||
var which: TvmVariable = .future_value;
|
||||
|
|
@ -250,7 +261,7 @@ pub fn solveTvm(params: TvmParams) CalcError!TvmSolution {
|
|||
unknowns += 1;
|
||||
which = .future_value;
|
||||
}
|
||||
if (unknowns != 1) return CalcError.InsufficientParameters;
|
||||
if (unknowns != 1) return Error.InsufficientParameters;
|
||||
|
||||
return switch (which) {
|
||||
.future_value => .{ .variable = which, .value = try solveFutureValue(params) },
|
||||
|
|
@ -261,41 +272,41 @@ pub fn solveTvm(params: TvmParams) CalcError!TvmSolution {
|
|||
};
|
||||
}
|
||||
|
||||
fn solveFutureValue(p: TvmParams) CalcError!f64 {
|
||||
fn solveFutureValue(p: TvmParams) Error!f64 {
|
||||
const r = p.rate.? / 100.0;
|
||||
const n = p.periods.?;
|
||||
if (r <= -1.0) return CalcError.DomainError;
|
||||
if (r <= -1.0) return Error.DomainError;
|
||||
return -(p.present_value.? * growth(r, n) + p.payment.? * annuityFactor(r, n, p.due));
|
||||
}
|
||||
|
||||
fn solvePresentValue(p: TvmParams) CalcError!f64 {
|
||||
fn solvePresentValue(p: TvmParams) Error!f64 {
|
||||
const r = p.rate.? / 100.0;
|
||||
const n = p.periods.?;
|
||||
if (r <= -1.0) return CalcError.DomainError;
|
||||
if (r <= -1.0) return Error.DomainError;
|
||||
const g = growth(r, n);
|
||||
if (g == 0) return CalcError.DivisionByZero;
|
||||
if (g == 0) return Error.DivisionByZero;
|
||||
return -(p.future_value.? + p.payment.? * annuityFactor(r, n, p.due)) / g;
|
||||
}
|
||||
|
||||
fn solvePayment(p: TvmParams) CalcError!f64 {
|
||||
fn solvePayment(p: TvmParams) Error!f64 {
|
||||
const r = p.rate.? / 100.0;
|
||||
const n = p.periods.?;
|
||||
if (r <= -1.0) return CalcError.DomainError;
|
||||
if (r <= -1.0) return Error.DomainError;
|
||||
const af = annuityFactor(r, n, p.due);
|
||||
if (af == 0) return CalcError.DivisionByZero;
|
||||
if (af == 0) return Error.DivisionByZero;
|
||||
return -(p.present_value.? * growth(r, n) + p.future_value.?) / af;
|
||||
}
|
||||
|
||||
fn solvePeriods(p: TvmParams) CalcError!f64 {
|
||||
fn solvePeriods(p: TvmParams) Error!f64 {
|
||||
const r = p.rate.? / 100.0;
|
||||
const pv = p.present_value.?;
|
||||
const pmt = p.payment.?;
|
||||
const fv = p.future_value.?;
|
||||
if (r <= -1.0) return CalcError.DomainError;
|
||||
if (r <= -1.0) return Error.DomainError;
|
||||
|
||||
// With no interest the equation is linear: pv + pmt*n + fv = 0.
|
||||
if (r == 0) {
|
||||
if (pmt == 0) return CalcError.InsufficientParameters;
|
||||
if (pmt == 0) return Error.InsufficientParameters;
|
||||
return -(pv + fv) / pmt;
|
||||
}
|
||||
|
||||
|
|
@ -304,14 +315,14 @@ fn solvePeriods(p: TvmParams) CalcError!f64 {
|
|||
const d: f64 = if (p.due) 1.0 + r else 1.0;
|
||||
const a = pmt * d / r;
|
||||
const denominator = pv + a;
|
||||
if (denominator == 0) return CalcError.DivisionByZero;
|
||||
if (denominator == 0) return Error.DivisionByZero;
|
||||
|
||||
const g = (a - fv) / denominator;
|
||||
// A non-positive growth factor has no real logarithm: the cash flows cannot
|
||||
// reach the requested future value at this rate.
|
||||
if (g <= 0) return CalcError.DomainError;
|
||||
if (g <= 0) return Error.DomainError;
|
||||
const base = 1.0 + r;
|
||||
if (base <= 0) return CalcError.DomainError;
|
||||
if (base <= 0) return Error.DomainError;
|
||||
return @log(g) / @log(base);
|
||||
}
|
||||
|
||||
|
|
@ -343,15 +354,15 @@ fn solvePeriods(p: TvmParams) CalcError!f64 {
|
|||
/// input that does have an answer (n=360 with r near 6% and a matching payment
|
||||
/// is one). Fixing that means scaling by the computed terms rather than by the
|
||||
/// inputs, which changes acceptance for every case and needs its own testing.
|
||||
fn solveRate(p: TvmParams) CalcError!TvmSolution {
|
||||
fn solveRate(p: TvmParams) Error!TvmSolution {
|
||||
const n = p.periods.?;
|
||||
const pv = p.present_value.?;
|
||||
const pmt = p.payment.?;
|
||||
const fv = p.future_value.?;
|
||||
if (n <= 0) return CalcError.DomainError;
|
||||
if (n <= 0) return Error.DomainError;
|
||||
|
||||
// A sign change in the cash flows is necessary for a solution to exist.
|
||||
if (pv == 0 and pmt == 0 and fv == 0) return CalcError.InsufficientParameters;
|
||||
if (pv == 0 and pmt == 0 and fv == 0) return Error.InsufficientParameters;
|
||||
|
||||
// Residuals are proportional to the size of the cash flows, so the
|
||||
// acceptance threshold has to be too.
|
||||
|
|
@ -396,7 +407,7 @@ fn solveRate(p: TvmParams) CalcError!TvmSolution {
|
|||
r = next;
|
||||
}
|
||||
}
|
||||
return CalcError.ConvergenceFailure;
|
||||
return Error.ConvergenceFailure;
|
||||
}
|
||||
|
||||
// -- Money rounding --
|
||||
|
|
@ -500,15 +511,15 @@ pub const AmortizationTotals = struct {
|
|||
};
|
||||
|
||||
/// The level payment implied by a loan, as a positive amount.
|
||||
pub fn amortizationPayment(p: AmortizationParams) CalcError!f64 {
|
||||
if (p.principal <= 0) return CalcError.DomainError;
|
||||
if (p.periods == 0 or p.periods > max_schedule_periods) return CalcError.DomainError;
|
||||
pub fn amortizationPayment(p: AmortizationParams) Error!f64 {
|
||||
if (p.principal <= 0) return Error.DomainError;
|
||||
if (p.periods == 0 or p.periods > max_schedule_periods) return Error.DomainError;
|
||||
// A negative rate would mean the balance shrinks on its own, which is not
|
||||
// something an amortization table describes.
|
||||
if (p.rate < 0) return CalcError.DomainError;
|
||||
if (p.rate < 0) return Error.DomainError;
|
||||
|
||||
if (p.payment) |given| {
|
||||
if (given <= 0) return CalcError.DomainError;
|
||||
if (given <= 0) return Error.DomainError;
|
||||
return if (p.round_cents) roundToCents(given) else given;
|
||||
}
|
||||
|
||||
|
|
@ -521,7 +532,7 @@ pub fn amortizationPayment(p: AmortizationParams) CalcError!f64 {
|
|||
// solveTvm returns the payment as a cash outflow; a schedule wants the
|
||||
// magnitude.
|
||||
const amount = -solution.value;
|
||||
if (!math.isFinite(amount) or amount <= 0) return CalcError.DomainError;
|
||||
if (!math.isFinite(amount) or amount <= 0) return Error.DomainError;
|
||||
return if (p.round_cents) roundToCents(amount) else amount;
|
||||
}
|
||||
|
||||
|
|
@ -537,7 +548,7 @@ const AmortizationCursor = struct {
|
|||
balance: f64,
|
||||
period: usize = 0,
|
||||
|
||||
fn init(p: AmortizationParams) CalcError!AmortizationCursor {
|
||||
fn init(p: AmortizationParams) Error!AmortizationCursor {
|
||||
const payment = try amortizationPayment(p);
|
||||
const rate = p.rate / 100.0;
|
||||
const balance = if (p.round_cents) roundToCents(p.principal) else p.principal;
|
||||
|
|
@ -546,7 +557,7 @@ const AmortizationCursor = struct {
|
|||
// reduces the balance: the loan grows forever, and there is no schedule
|
||||
// to print.
|
||||
const first_interest = balance * rate;
|
||||
if (payment <= first_interest) return CalcError.DomainError;
|
||||
if (payment <= first_interest) return Error.DomainError;
|
||||
|
||||
return .{ .params = p, .payment = payment, .rate = rate, .balance = balance };
|
||||
}
|
||||
|
|
@ -584,21 +595,21 @@ const AmortizationCursor = struct {
|
|||
};
|
||||
|
||||
/// A single period of a schedule, without building the whole table.
|
||||
pub fn amortizationEntry(p: AmortizationParams, period: usize) CalcError!AmortizationEntry {
|
||||
if (period == 0) return CalcError.DomainError;
|
||||
pub fn amortizationEntry(p: AmortizationParams, period: usize) Error!AmortizationEntry {
|
||||
if (period == 0) return Error.DomainError;
|
||||
var cursor = try AmortizationCursor.init(p);
|
||||
while (cursor.next()) |entry| {
|
||||
if (entry.period == period) return entry;
|
||||
}
|
||||
// The loan was retired before this period, so the period does not exist.
|
||||
return CalcError.DomainError;
|
||||
return Error.DomainError;
|
||||
}
|
||||
|
||||
/// The full schedule. Caller owns the returned slice.
|
||||
pub fn amortizationSchedule(
|
||||
allocator: std.mem.Allocator,
|
||||
p: AmortizationParams,
|
||||
) CalcError![]AmortizationEntry {
|
||||
) Error![]AmortizationEntry {
|
||||
var cursor = try AmortizationCursor.init(p);
|
||||
var rows: std.ArrayList(AmortizationEntry) = .empty;
|
||||
errdefer rows.deinit(allocator);
|
||||
|
|
@ -609,7 +620,7 @@ pub fn amortizationSchedule(
|
|||
}
|
||||
|
||||
/// Schedule totals, computed without allocating a table.
|
||||
pub fn amortizationTotals(p: AmortizationParams) CalcError!AmortizationTotals {
|
||||
pub fn amortizationTotals(p: AmortizationParams) Error!AmortizationTotals {
|
||||
var cursor = try AmortizationCursor.init(p);
|
||||
var totals: AmortizationTotals = .{ .periods = 0, .paid = 0, .interest = 0, .principal = 0 };
|
||||
while (cursor.next()) |entry| {
|
||||
|
|
@ -661,11 +672,11 @@ test "cagr: single period is the simple return" {
|
|||
}
|
||||
|
||||
test "cagr: domain errors" {
|
||||
try testing.expectError(CalcError.DomainError, cagr(1000, 2000, 0));
|
||||
try testing.expectError(CalcError.DomainError, cagr(1000, 2000, -5));
|
||||
try testing.expectError(CalcError.DomainError, cagr(0, 2000, 5));
|
||||
try testing.expectError(CalcError.DomainError, cagr(-1000, 2000, 5));
|
||||
try testing.expectError(CalcError.DomainError, cagr(1000, -1, 5));
|
||||
try testing.expectError(Error.DomainError, cagr(1000, 2000, 0));
|
||||
try testing.expectError(Error.DomainError, cagr(1000, 2000, -5));
|
||||
try testing.expectError(Error.DomainError, cagr(0, 2000, 5));
|
||||
try testing.expectError(Error.DomainError, cagr(-1000, 2000, 5));
|
||||
try testing.expectError(Error.DomainError, cagr(1000, -1, 5));
|
||||
}
|
||||
|
||||
test "compound interest: annual compounding" {
|
||||
|
|
@ -707,11 +718,11 @@ test "compound interest: present value textbook figure" {
|
|||
}
|
||||
|
||||
test "compound interest: domain errors" {
|
||||
try testing.expectError(CalcError.DomainError, compoundFutureValue(1000, 5, 10, 0));
|
||||
try testing.expectError(CalcError.DomainError, compoundFutureValue(1000, 5, -1, 12));
|
||||
try testing.expectError(CalcError.DomainError, compoundPresentValue(1000, 5, 10, 0));
|
||||
try testing.expectError(Error.DomainError, compoundFutureValue(1000, 5, 10, 0));
|
||||
try testing.expectError(Error.DomainError, compoundFutureValue(1000, 5, -1, 12));
|
||||
try testing.expectError(Error.DomainError, compoundPresentValue(1000, 5, 10, 0));
|
||||
// A rate of -100% per period wipes the base out entirely.
|
||||
try testing.expectError(CalcError.DomainError, compoundFutureValue(1000, -1200, 10, 12));
|
||||
try testing.expectError(Error.DomainError, compoundFutureValue(1000, -1200, 10, 12));
|
||||
}
|
||||
|
||||
test "tvm: solve payment for a classic 30-year mortgage" {
|
||||
|
|
@ -880,7 +891,7 @@ test "tvm: annuity due round-trips too" {
|
|||
|
||||
test "tvm: requires exactly one unknown" {
|
||||
// All five supplied.
|
||||
try testing.expectError(CalcError.InsufficientParameters, solveTvm(.{
|
||||
try testing.expectError(Error.InsufficientParameters, solveTvm(.{
|
||||
.periods = 10,
|
||||
.rate = 5,
|
||||
.present_value = 100,
|
||||
|
|
@ -888,13 +899,13 @@ test "tvm: requires exactly one unknown" {
|
|||
.future_value = 0,
|
||||
}));
|
||||
// Two unknowns.
|
||||
try testing.expectError(CalcError.InsufficientParameters, solveTvm(.{
|
||||
try testing.expectError(Error.InsufficientParameters, solveTvm(.{
|
||||
.periods = 10,
|
||||
.rate = 5,
|
||||
.present_value = 100,
|
||||
}));
|
||||
// Nothing supplied at all.
|
||||
try testing.expectError(CalcError.InsufficientParameters, solveTvm(.{}));
|
||||
try testing.expectError(Error.InsufficientParameters, solveTvm(.{}));
|
||||
}
|
||||
|
||||
test "tvm: unsolvable cash flows report convergence failure, not a wrong answer" {
|
||||
|
|
@ -905,17 +916,17 @@ test "tvm: unsolvable cash flows report convergence failure, not a wrong answer"
|
|||
.payment = 100,
|
||||
.future_value = 5000,
|
||||
});
|
||||
try testing.expectError(CalcError.ConvergenceFailure, result);
|
||||
try testing.expectError(Error.ConvergenceFailure, result);
|
||||
}
|
||||
|
||||
test "tvm: rate solver rejects degenerate input" {
|
||||
try testing.expectError(CalcError.InsufficientParameters, solveTvm(.{
|
||||
try testing.expectError(Error.InsufficientParameters, solveTvm(.{
|
||||
.periods = 10,
|
||||
.present_value = 0,
|
||||
.payment = 0,
|
||||
.future_value = 0,
|
||||
}));
|
||||
try testing.expectError(CalcError.DomainError, solveTvm(.{
|
||||
try testing.expectError(Error.DomainError, solveTvm(.{
|
||||
.periods = 0,
|
||||
.present_value = -100,
|
||||
.payment = 0,
|
||||
|
|
@ -925,7 +936,7 @@ test "tvm: rate solver rejects degenerate input" {
|
|||
|
||||
test "tvm: unreachable future value has no real period count" {
|
||||
// Paying nothing can never grow 1000 into 5000.
|
||||
try testing.expectError(CalcError.DomainError, solveTvm(.{
|
||||
try testing.expectError(Error.DomainError, solveTvm(.{
|
||||
.rate = 5,
|
||||
.present_value = 1000,
|
||||
.payment = 0,
|
||||
|
|
@ -934,7 +945,7 @@ test "tvm: unreachable future value has no real period count" {
|
|||
}
|
||||
|
||||
test "tvm: periods with zero rate and zero payment is unsolvable" {
|
||||
try testing.expectError(CalcError.InsufficientParameters, solveTvm(.{
|
||||
try testing.expectError(Error.InsufficientParameters, solveTvm(.{
|
||||
.rate = 0,
|
||||
.present_value = 1000,
|
||||
.payment = 0,
|
||||
|
|
@ -1181,13 +1192,13 @@ test "amortization: an underfunded term ends in a balloon payment" {
|
|||
|
||||
test "amortization: a payment below the first interest charge is rejected" {
|
||||
// 1000 of interest in month one, so 500 never touches principal.
|
||||
try testing.expectError(CalcError.DomainError, amortizationPayment(.{
|
||||
try testing.expectError(Error.DomainError, amortizationPayment(.{
|
||||
.principal = 200000,
|
||||
.rate = 0.5,
|
||||
.periods = 360,
|
||||
.payment = -1,
|
||||
}));
|
||||
try testing.expectError(CalcError.DomainError, amortizationEntry(.{
|
||||
try testing.expectError(Error.DomainError, amortizationEntry(.{
|
||||
.principal = 200000,
|
||||
.rate = 0.5,
|
||||
.periods = 360,
|
||||
|
|
@ -1196,22 +1207,22 @@ test "amortization: a payment below the first interest charge is rejected" {
|
|||
}
|
||||
|
||||
test "amortization: rejects nonsense loan terms" {
|
||||
try testing.expectError(CalcError.DomainError, amortizationPayment(.{
|
||||
try testing.expectError(Error.DomainError, amortizationPayment(.{
|
||||
.principal = 0,
|
||||
.rate = 0.5,
|
||||
.periods = 12,
|
||||
}));
|
||||
try testing.expectError(CalcError.DomainError, amortizationPayment(.{
|
||||
try testing.expectError(Error.DomainError, amortizationPayment(.{
|
||||
.principal = 1000,
|
||||
.rate = 0.5,
|
||||
.periods = 0,
|
||||
}));
|
||||
try testing.expectError(CalcError.DomainError, amortizationPayment(.{
|
||||
try testing.expectError(Error.DomainError, amortizationPayment(.{
|
||||
.principal = 1000,
|
||||
.rate = -1,
|
||||
.periods = 12,
|
||||
}));
|
||||
try testing.expectError(CalcError.DomainError, amortizationPayment(.{
|
||||
try testing.expectError(Error.DomainError, amortizationPayment(.{
|
||||
.principal = 1000,
|
||||
.rate = 0.5,
|
||||
.periods = max_schedule_periods + 1,
|
||||
|
|
@ -1220,8 +1231,8 @@ test "amortization: rejects nonsense loan terms" {
|
|||
|
||||
test "amortization: periods outside the schedule are an error" {
|
||||
const params: AmortizationParams = .{ .principal = 1200, .rate = 0, .periods = 12 };
|
||||
try testing.expectError(CalcError.DomainError, amortizationEntry(params, 0));
|
||||
try testing.expectError(CalcError.DomainError, amortizationEntry(params, 13));
|
||||
try testing.expectError(Error.DomainError, amortizationEntry(params, 0));
|
||||
try testing.expectError(Error.DomainError, amortizationEntry(params, 13));
|
||||
}
|
||||
|
||||
test "amortization: unrounded mode keeps full precision" {
|
||||
|
|
@ -1252,7 +1263,7 @@ test "amortization: schedule allocation failure frees the partial table" {
|
|||
allocator.free(rows);
|
||||
return;
|
||||
} else |err| {
|
||||
try testing.expectEqual(CalcError.OutOfMemory, err);
|
||||
try testing.expectEqual(Error.OutOfMemory, err);
|
||||
}
|
||||
}
|
||||
return error.AllocationSweepNeverCompleted;
|
||||
|
|
@ -1300,18 +1311,18 @@ test "compoundRate: unchanged value is a zero rate" {
|
|||
|
||||
test "compoundRate: domain errors" {
|
||||
// No time elapsed: nothing to solve.
|
||||
try testing.expectError(CalcError.DomainError, compoundRate(1000, 2000, 0, 1));
|
||||
try testing.expectError(CalcError.DomainError, compoundRate(1000, 2000, -5, 1));
|
||||
try testing.expectError(Error.DomainError, compoundRate(1000, 2000, 0, 1));
|
||||
try testing.expectError(Error.DomainError, compoundRate(1000, 2000, -5, 1));
|
||||
// No compounding frequency.
|
||||
try testing.expectError(CalcError.DomainError, compoundRate(1000, 2000, 10, 0));
|
||||
try testing.expectError(CalcError.DomainError, compoundRate(1000, 2000, 10, -12));
|
||||
try testing.expectError(Error.DomainError, compoundRate(1000, 2000, 10, 0));
|
||||
try testing.expectError(Error.DomainError, compoundRate(1000, 2000, 10, -12));
|
||||
// Nothing to grow from.
|
||||
try testing.expectError(CalcError.DomainError, compoundRate(0, 2000, 10, 1));
|
||||
try testing.expectError(Error.DomainError, compoundRate(0, 2000, 10, 1));
|
||||
// A sign change has no real root.
|
||||
try testing.expectError(CalcError.DomainError, compoundRate(1000, -500, 10, 1));
|
||||
try testing.expectError(CalcError.DomainError, compoundRate(-1000, 500, 10, 1));
|
||||
try testing.expectError(Error.DomainError, compoundRate(1000, -500, 10, 1));
|
||||
try testing.expectError(Error.DomainError, compoundRate(-1000, 500, 10, 1));
|
||||
// Reaching exactly zero would need a rate of -100%, which is a limit.
|
||||
try testing.expectError(CalcError.DomainError, compoundRate(1000, 0, 10, 1));
|
||||
try testing.expectError(Error.DomainError, compoundRate(1000, 0, 10, 1));
|
||||
}
|
||||
|
||||
test "compoundPeriods: inverts compoundFutureValue" {
|
||||
|
|
@ -1350,13 +1361,13 @@ test "compoundPeriods: already there takes no time at all" {
|
|||
|
||||
test "compoundPeriods: domain errors" {
|
||||
// A zero rate never reaches a different value.
|
||||
try testing.expectError(CalcError.DomainError, compoundPeriods(1000, 2000, 0, 1));
|
||||
try testing.expectError(Error.DomainError, compoundPeriods(1000, 2000, 0, 1));
|
||||
// -100% or worse is not a rate.
|
||||
try testing.expectError(CalcError.DomainError, compoundPeriods(1000, 2000, -100, 1));
|
||||
try testing.expectError(CalcError.DomainError, compoundPeriods(1000, 2000, -150, 1));
|
||||
try testing.expectError(CalcError.DomainError, compoundPeriods(1000, 2000, 5, 0));
|
||||
try testing.expectError(CalcError.DomainError, compoundPeriods(0, 2000, 5, 1));
|
||||
try testing.expectError(CalcError.DomainError, compoundPeriods(1000, -2000, 5, 1));
|
||||
try testing.expectError(Error.DomainError, compoundPeriods(1000, 2000, -100, 1));
|
||||
try testing.expectError(Error.DomainError, compoundPeriods(1000, 2000, -150, 1));
|
||||
try testing.expectError(Error.DomainError, compoundPeriods(1000, 2000, 5, 0));
|
||||
try testing.expectError(Error.DomainError, compoundPeriods(0, 2000, 5, 1));
|
||||
try testing.expectError(Error.DomainError, compoundPeriods(1000, -2000, 5, 1));
|
||||
}
|
||||
|
||||
test "effectiveAnnualRate: monthly compounding beats its nominal rate" {
|
||||
|
|
@ -1376,8 +1387,8 @@ test "effectiveAnnualRate: a negative nominal rate stays negative" {
|
|||
}
|
||||
|
||||
test "effectiveAnnualRate: domain errors" {
|
||||
try testing.expectError(CalcError.DomainError, effectiveAnnualRate(5, 0));
|
||||
try testing.expectError(CalcError.DomainError, effectiveAnnualRate(-100, 1));
|
||||
try testing.expectError(Error.DomainError, effectiveAnnualRate(5, 0));
|
||||
try testing.expectError(Error.DomainError, effectiveAnnualRate(-100, 1));
|
||||
}
|
||||
|
||||
test "compound interest: the four variables round-trip through each other" {
|
||||
|
|
|
|||
|
|
@ -19,29 +19,17 @@
|
|||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const rational = @import("rational.zig");
|
||||
const Rational = rational.Rational;
|
||||
const CalcError = @import("errors.zig").CalcError;
|
||||
const Rational = @import("Rational.zig");
|
||||
|
||||
/// Map a numeric-model error onto the engine's error set.
|
||||
/// The errors arithmetic on `Number` can produce, which are `Rational`'s: this tier
|
||||
/// adds no failure of its own.
|
||||
///
|
||||
/// Lives here so there is one mapping. The evaluator and the unit converter each
|
||||
/// had their own copy, which is how they came to disagree: one turned
|
||||
/// `ExponentTooLarge` into `Overflow` and neither knew what to do with a newly
|
||||
/// added member until the compiler complained in two places.
|
||||
pub fn toCalcError(err: Error) CalcError {
|
||||
return switch (err) {
|
||||
error.OutOfMemory => CalcError.OutOfMemory,
|
||||
error.DivisionByZero => CalcError.DivisionByZero,
|
||||
error.InvalidNumber => CalcError.InvalidNumber,
|
||||
// An exponent too large to compute is an overflow from the caller's view.
|
||||
error.ExponentTooLarge => CalcError.Overflow,
|
||||
// The square root of a negative value is outside the domain.
|
||||
error.NegativeRoot => CalcError.DomainError,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Error = rational.Error;
|
||||
/// There used to be a `toCalcError` here translating these into a single engine-wide
|
||||
/// error set, because every module returned that one set. The names it translated
|
||||
/// away were the more useful ones: `ExponentTooLarge` became `Overflow` and
|
||||
/// `NegativeRoot` became `DomainError`, so `sqrt(-1)` reported "domain error" when
|
||||
/// the engine knew exactly what was wrong.
|
||||
pub const Error = Rational.Error;
|
||||
|
||||
/// Denominator size at which an exact result is demoted to inexact.
|
||||
///
|
||||
|
|
@ -617,31 +605,12 @@ test "sqrt: a negative input is a domain error, not a silent NaN" {
|
|||
try testing.expect(root_zero.isExact());
|
||||
}
|
||||
|
||||
test "sqrt: the domain error reaches the engine error set as a domain error" {
|
||||
test "sqrt: a negative input surfaces as NegativeRoot, not a vaguer error" {
|
||||
var neg = try Number.fromInt(alloc, -4);
|
||||
defer neg.deinit();
|
||||
try testing.expectError(Error.NegativeRoot, Number.sqrt(alloc, neg));
|
||||
}
|
||||
|
||||
test "toCalcError maps every numeric error, with no default" {
|
||||
const CalcErr = @import("errors.zig").CalcError;
|
||||
// One mapping for the evaluator and the unit converter, which used to have a
|
||||
// copy each. Walking the whole set keeps the two tiers of error vocabulary
|
||||
// lined up: a member added to Error has to be given a CalcError here.
|
||||
try testing.expectEqual(CalcErr.OutOfMemory, toCalcError(Error.OutOfMemory));
|
||||
try testing.expectEqual(CalcErr.DivisionByZero, toCalcError(Error.DivisionByZero));
|
||||
try testing.expectEqual(CalcErr.InvalidNumber, toCalcError(Error.InvalidNumber));
|
||||
try testing.expectEqual(CalcErr.Overflow, toCalcError(Error.ExponentTooLarge));
|
||||
try testing.expectEqual(CalcErr.DomainError, toCalcError(Error.NegativeRoot));
|
||||
|
||||
inline for (@typeInfo(Error).error_set.?) |field| {
|
||||
// Every member is handled: this would not compile past an unhandled one,
|
||||
// and every mapping lands in the engine's error set.
|
||||
const mapped = toCalcError(@field(Error, field.name));
|
||||
try testing.expect(@TypeOf(mapped) == CalcErr);
|
||||
}
|
||||
}
|
||||
|
||||
test "asExactInt" {
|
||||
var a = try Number.fromInt(alloc, 42);
|
||||
defer a.deinit();
|
||||
|
|
@ -911,7 +880,7 @@ test "max and min with an inexact operand return that operand as-is" {
|
|||
|
||||
// -- Allocation-failure safety --
|
||||
//
|
||||
// Mirrors the sweep in rational.zig: fail the Nth allocation for every N, and
|
||||
// Mirrors the sweep in Rational.zig: fail the Nth allocation for every N, and
|
||||
// let testing.allocator's leak detection verify that partially-built values are
|
||||
// released. This is what actually validates the cleanup paths; merely executing
|
||||
// them proves nothing.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,21 @@ const Tokenizer = tokenizer_mod.Tokenizer;
|
|||
const TokenKind = tokenizer_mod.TokenKind;
|
||||
const Token = tokenizer_mod.Token;
|
||||
const parseNumber = tokenizer_mod.parseNumber;
|
||||
const CalcError = @import("errors.zig").CalcError;
|
||||
|
||||
/// What parsing can fail with.
|
||||
///
|
||||
/// Declared here rather than shared: the parser used to return a single engine-wide
|
||||
/// error set, so its signature claimed it might return `ConvergenceFailure`,
|
||||
/// `UnknownUnit` and `StructTooLarge`. No caller could switch on what a parse can
|
||||
/// actually produce.
|
||||
pub const Error = error{
|
||||
UnexpectedToken,
|
||||
UnmatchedParen,
|
||||
UnexpectedEnd,
|
||||
InvalidExpression,
|
||||
InvalidNumber,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
/// Precedence levels (higher = tighter binding).
|
||||
///
|
||||
|
|
@ -92,19 +106,19 @@ pub const Parser = struct {
|
|||
/// path below frees what it built. Without that, a single typo in an
|
||||
/// interactive session leaks the partial tree, which is exactly what the TUI
|
||||
/// does on every keystroke-completed expression.
|
||||
pub fn parse(self: *Parser) CalcError!*Expr {
|
||||
pub fn parse(self: *Parser) Error!*Expr {
|
||||
const expr = try self.parseExpr(.none);
|
||||
if (self.current.kind != .eof) {
|
||||
// Trailing tokens: the tree parsed so far is unreachable.
|
||||
freeExpr(self.allocator, expr);
|
||||
return CalcError.UnexpectedToken;
|
||||
return Error.UnexpectedToken;
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
/// Parse an expression with the given minimum precedence.
|
||||
fn parseExpr(self: *Parser, min_prec: Prec) CalcError!*Expr {
|
||||
if (self.nest_depth >= max_nest_depth) return CalcError.InvalidExpression;
|
||||
fn parseExpr(self: *Parser, min_prec: Prec) Error!*Expr {
|
||||
if (self.nest_depth >= max_nest_depth) return Error.InvalidExpression;
|
||||
self.nest_depth += 1;
|
||||
defer self.nest_depth -= 1;
|
||||
|
||||
|
|
@ -124,13 +138,13 @@ pub const Parser = struct {
|
|||
}
|
||||
|
||||
/// Parse a prefix expression (number, identifier, unary op, parenthesized).
|
||||
fn parsePrefix(self: *Parser) CalcError!*Expr {
|
||||
fn parsePrefix(self: *Parser) Error!*Expr {
|
||||
const tok = self.current;
|
||||
switch (tok.kind) {
|
||||
.number => {
|
||||
self.advance();
|
||||
const text = tok.text(self.source);
|
||||
const num = parseNumber(text) catch return CalcError.InvalidNumber;
|
||||
const num = parseNumber(text) catch return Error.InvalidNumber;
|
||||
return self.makeNode(.{ .number = .{
|
||||
.float_value = num.float,
|
||||
.int_value = num.int_value,
|
||||
|
|
@ -142,7 +156,7 @@ pub const Parser = struct {
|
|||
self.advance();
|
||||
const text = tok.text(self.source);
|
||||
// Strip quotes: 'abc' -> abc
|
||||
if (text.len < 2) return CalcError.InvalidNumber;
|
||||
if (text.len < 2) return Error.InvalidNumber;
|
||||
const content = text[1 .. text.len - 1];
|
||||
return self.makeNode(.{ .string_literal = content });
|
||||
},
|
||||
|
|
@ -183,7 +197,7 @@ pub const Parser = struct {
|
|||
const first_arg = try self.parseExpr(.none);
|
||||
args.append(self.allocator, first_arg) catch {
|
||||
freeExpr(self.allocator, first_arg);
|
||||
return CalcError.OutOfMemory;
|
||||
return Error.OutOfMemory;
|
||||
};
|
||||
|
||||
while (self.current.kind == .comma) {
|
||||
|
|
@ -191,18 +205,18 @@ pub const Parser = struct {
|
|||
const arg = try self.parseExpr(.none);
|
||||
args.append(self.allocator, arg) catch {
|
||||
freeExpr(self.allocator, arg);
|
||||
return CalcError.OutOfMemory;
|
||||
return Error.OutOfMemory;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (self.current.kind != .right_paren) {
|
||||
return CalcError.UnmatchedParen;
|
||||
return Error.UnmatchedParen;
|
||||
}
|
||||
self.advance(); // consume )
|
||||
|
||||
const args_slice = self.allocator.dupe(*Expr, args.items) catch
|
||||
return CalcError.OutOfMemory;
|
||||
return Error.OutOfMemory;
|
||||
errdefer self.allocator.free(args_slice);
|
||||
|
||||
return self.makeNode(.{ .call = .{
|
||||
|
|
@ -221,7 +235,7 @@ pub const Parser = struct {
|
|||
const inner = try self.parseExpr(.none);
|
||||
if (self.current.kind != .right_paren) {
|
||||
freeExpr(self.allocator, inner);
|
||||
return CalcError.UnmatchedParen;
|
||||
return Error.UnmatchedParen;
|
||||
}
|
||||
self.advance(); // consume )
|
||||
return inner;
|
||||
|
|
@ -245,16 +259,16 @@ pub const Parser = struct {
|
|||
} });
|
||||
},
|
||||
.eof => {
|
||||
return CalcError.UnexpectedEnd;
|
||||
return Error.UnexpectedEnd;
|
||||
},
|
||||
else => {
|
||||
return CalcError.UnexpectedToken;
|
||||
return Error.UnexpectedToken;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an infix expression given the left-hand side and precedence.
|
||||
fn parseInfix(self: *Parser, left: *Expr, prec: Prec) CalcError!*Expr {
|
||||
fn parseInfix(self: *Parser, left: *Expr, prec: Prec) Error!*Expr {
|
||||
const tok = self.current;
|
||||
|
||||
// Handle keyword operators (rol, ror, and, or, xor)
|
||||
|
|
@ -276,7 +290,7 @@ pub const Parser = struct {
|
|||
|
||||
self.advance();
|
||||
const op = self.tokenToBinaryOp(tok.kind) orelse {
|
||||
return CalcError.UnexpectedToken;
|
||||
return Error.UnexpectedToken;
|
||||
};
|
||||
|
||||
// Right-associative for power
|
||||
|
|
@ -352,13 +366,13 @@ pub const Parser = struct {
|
|||
self.current = self.tokenizer.next();
|
||||
}
|
||||
|
||||
fn makeNode(self: *Parser, expr: Expr) CalcError!*Expr {
|
||||
fn makeNode(self: *Parser, expr: Expr) Error!*Expr {
|
||||
// Budget checked here so every construction site is covered by one test.
|
||||
if (self.node_count >= max_nodes) {
|
||||
return CalcError.InvalidExpression;
|
||||
return Error.InvalidExpression;
|
||||
}
|
||||
self.node_count += 1;
|
||||
const node = self.allocator.create(Expr) catch return CalcError.OutOfMemory;
|
||||
const node = self.allocator.create(Expr) catch return Error.OutOfMemory;
|
||||
node.* = expr;
|
||||
return node;
|
||||
}
|
||||
|
|
@ -378,7 +392,7 @@ fn testParse(source: []const u8) !*Expr {
|
|||
return parser.parse();
|
||||
}
|
||||
|
||||
fn testParseArena(source: []const u8) CalcError!*Expr {
|
||||
fn testParseArena(source: []const u8) Error!*Expr {
|
||||
const alloc = test_arena_instance.allocator();
|
||||
var p = Parser.init(alloc, source);
|
||||
return p.parse();
|
||||
|
|
@ -501,19 +515,19 @@ test "parse assignment" {
|
|||
test "parse adjacent number and identifier is an error (no implicit mul)" {
|
||||
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||
const result = testParseArena("2pi");
|
||||
try testing.expectError(CalcError.UnexpectedToken, result);
|
||||
try testing.expectError(Error.UnexpectedToken, result);
|
||||
}
|
||||
|
||||
test "parse adjacent number and paren is an error (no implicit mul)" {
|
||||
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||
const result = testParseArena("3(4+5)");
|
||||
try testing.expectError(CalcError.UnexpectedToken, result);
|
||||
try testing.expectError(Error.UnexpectedToken, result);
|
||||
}
|
||||
|
||||
test "parse adjacent paren paren is an error (no implicit mul)" {
|
||||
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||
const result = testParseArena("(2)(3)");
|
||||
try testing.expectError(CalcError.UnexpectedToken, result);
|
||||
try testing.expectError(Error.UnexpectedToken, result);
|
||||
}
|
||||
|
||||
test "parse caret is power in programmer mode (not XOR)" {
|
||||
|
|
@ -581,20 +595,20 @@ test "parse bitwise not" {
|
|||
test "parse error: unmatched paren" {
|
||||
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||
const result = testParseArena("(2 + 3");
|
||||
try testing.expectError(CalcError.UnmatchedParen, result);
|
||||
try testing.expectError(Error.UnmatchedParen, result);
|
||||
}
|
||||
|
||||
test "parse error: unexpected token" {
|
||||
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||
const result = testParseArena("+ +");
|
||||
// + at start is not a valid prefix
|
||||
try testing.expectError(CalcError.UnexpectedToken, result);
|
||||
try testing.expectError(Error.UnexpectedToken, result);
|
||||
}
|
||||
|
||||
test "parse error: empty expression" {
|
||||
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||
const result = testParseArena("");
|
||||
try testing.expectError(CalcError.UnexpectedEnd, result);
|
||||
try testing.expectError(Error.UnexpectedEnd, result);
|
||||
}
|
||||
|
||||
test "parse complex expression" {
|
||||
|
|
@ -617,7 +631,7 @@ test "parse nested function calls" {
|
|||
test "parse error: unmatched paren in function call args" {
|
||||
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||
const result = testParseArena("max(1, 2");
|
||||
try testing.expectError(CalcError.UnmatchedParen, result);
|
||||
try testing.expectError(Error.UnmatchedParen, result);
|
||||
}
|
||||
|
||||
test "parse error: identifier in infix position (not a keyword op)" {
|
||||
|
|
@ -626,7 +640,7 @@ test "parse error: identifier in infix position (not a keyword op)" {
|
|||
// parse() reports the leftover token as unexpected.
|
||||
defer _ = test_arena_instance.reset(.retain_capacity);
|
||||
const result = testParseArena("5 foo");
|
||||
try testing.expectError(CalcError.UnexpectedToken, result);
|
||||
try testing.expectError(Error.UnexpectedToken, result);
|
||||
}
|
||||
|
||||
// -- Ownership on the error paths --
|
||||
|
|
@ -725,7 +739,7 @@ test "an allocation failure mid-parse frees whatever was built" {
|
|||
freeExpr(allocator, expr);
|
||||
break;
|
||||
} else |err| {
|
||||
try testing.expectEqual(CalcError.OutOfMemory, err);
|
||||
try testing.expectEqual(Error.OutOfMemory, err);
|
||||
}
|
||||
} else {
|
||||
return error.AllocationSweepNeverCompleted;
|
||||
|
|
@ -751,7 +765,7 @@ test "a tree larger than the node budget is rejected, not built" {
|
|||
}
|
||||
|
||||
var parser = Parser.init(testing.allocator, over.items);
|
||||
try testing.expectError(CalcError.InvalidExpression, parser.parse());
|
||||
try testing.expectError(Error.InvalidExpression, parser.parse());
|
||||
// Nothing is left allocated: testing.allocator would report a leak otherwise.
|
||||
}
|
||||
|
||||
|
|
@ -777,7 +791,7 @@ test "nesting deeper than the depth limit is rejected" {
|
|||
for (0..Parser.max_nest_depth + 10) |_| try deep.append(testing.allocator, ')');
|
||||
|
||||
var parser = Parser.init(testing.allocator, deep.items);
|
||||
try testing.expectError(CalcError.InvalidExpression, parser.parse());
|
||||
try testing.expectError(Error.InvalidExpression, parser.parse());
|
||||
}
|
||||
|
||||
test "nesting within the depth limit parses" {
|
||||
|
|
@ -811,7 +825,7 @@ test "the depth limit also covers nested calls and unary operators" {
|
|||
for (0..Parser.max_nest_depth + 10) |_| try deep.append(testing.allocator, ')');
|
||||
|
||||
var parser = Parser.init(testing.allocator, deep.items);
|
||||
try testing.expectError(CalcError.InvalidExpression, parser.parse());
|
||||
try testing.expectError(Error.InvalidExpression, parser.parse());
|
||||
|
||||
var unary = std.ArrayList(u8).empty;
|
||||
defer unary.deinit(testing.allocator);
|
||||
|
|
@ -819,5 +833,5 @@ test "the depth limit also covers nested calls and unary operators" {
|
|||
try unary.append(testing.allocator, '1');
|
||||
|
||||
var unary_parser = Parser.init(testing.allocator, unary.items);
|
||||
try testing.expectError(CalcError.InvalidExpression, unary_parser.parse());
|
||||
try testing.expectError(Error.InvalidExpression, unary_parser.parse());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,18 @@ const BinaryOp = ast.BinaryOp;
|
|||
const Integer = @import("Integer.zig");
|
||||
const IntType = Integer.IntType;
|
||||
const Endianness = std.builtin.Endian;
|
||||
const CalcError = @import("errors.zig").CalcError;
|
||||
|
||||
/// What programmer-mode evaluation can fail with: the parse, the fixed-width
|
||||
/// operators, and its own arithmetic and name errors.
|
||||
pub const Error = error{
|
||||
DivisionByZero,
|
||||
DomainError,
|
||||
InvalidNumber,
|
||||
InvalidOperandType,
|
||||
Overflow,
|
||||
UnknownFunction,
|
||||
UnknownVariable,
|
||||
} || parser_mod.Error || bitwise.Error;
|
||||
const parser_mod = @import("parser.zig");
|
||||
const Parser = parser_mod.Parser;
|
||||
const bitwise = @import("bitwise.zig");
|
||||
|
|
@ -29,7 +40,7 @@ pub const Config = struct {
|
|||
};
|
||||
|
||||
/// Evaluate an AST in programmer mode, producing an exact integer result.
|
||||
pub fn evalProgrammer(config: Config, expr: *const Expr) CalcError!Integer {
|
||||
pub fn evalProgrammer(config: Config, expr: *const Expr) Error!Integer {
|
||||
return .{
|
||||
.raw = try evalExpr(config, expr),
|
||||
.int_type = config.int_type,
|
||||
|
|
@ -37,7 +48,7 @@ pub fn evalProgrammer(config: Config, expr: *const Expr) CalcError!Integer {
|
|||
}
|
||||
|
||||
/// Recursively evaluate an expression to a raw u128.
|
||||
fn evalExpr(config: Config, expr: *const Expr) CalcError!u128 {
|
||||
fn evalExpr(config: Config, expr: *const Expr) Error!u128 {
|
||||
switch (expr.*) {
|
||||
.number => |n| {
|
||||
if (n.int_value) |int_val| {
|
||||
|
|
@ -51,8 +62,8 @@ fn evalExpr(config: Config, expr: *const Expr) CalcError!u128 {
|
|||
// value is illegal behaviour, and `tally -p '1e40'` aborted the process
|
||||
// before this guard existed.
|
||||
const value = n.float_value;
|
||||
if (!std.math.isFinite(value) or value < 0) return CalcError.DomainError;
|
||||
if (value >= 340282366920938463463374607431768211456.0) return CalcError.Overflow;
|
||||
if (!std.math.isFinite(value) or value < 0) return Error.DomainError;
|
||||
if (value >= 340282366920938463463374607431768211456.0) return Error.Overflow;
|
||||
const val: u128 = @intFromFloat(value);
|
||||
return val & config.int_type.mask();
|
||||
},
|
||||
|
|
@ -60,19 +71,19 @@ fn evalExpr(config: Config, expr: *const Expr) CalcError!u128 {
|
|||
// Pack ASCII bytes into integer.
|
||||
// Big-endian packing: first char -> most significant used byte.
|
||||
const max_bytes = @as(usize, config.int_type.bits()) / 8;
|
||||
if (text.len > max_bytes) return CalcError.Overflow;
|
||||
if (text.len > max_bytes) return Error.Overflow;
|
||||
var result: u128 = 0;
|
||||
for (text) |byte| {
|
||||
if (byte > 0x7F) return CalcError.InvalidNumber;
|
||||
if (byte > 0x7F) return Error.InvalidNumber;
|
||||
result = (result << 8) | byte;
|
||||
}
|
||||
return result & config.int_type.mask();
|
||||
},
|
||||
.variable => {
|
||||
return CalcError.UnknownVariable;
|
||||
return Error.UnknownVariable;
|
||||
},
|
||||
.assignment => {
|
||||
return CalcError.InvalidOperandType;
|
||||
return Error.InvalidOperandType;
|
||||
},
|
||||
.unary => |u| {
|
||||
const operand = try evalExpr(config, u.operand);
|
||||
|
|
@ -89,7 +100,7 @@ fn evalExpr(config: Config, expr: *const Expr) CalcError!u128 {
|
|||
},
|
||||
.call => {
|
||||
// No function calls in programmer mode
|
||||
return CalcError.UnknownFunction;
|
||||
return Error.UnknownFunction;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +111,7 @@ fn evalExpr(config: Config, expr: *const Expr) CalcError!u128 {
|
|||
/// `bitwise.zig`, which standard mode uses too, so the two modes cannot drift
|
||||
/// apart again. What remains is the arithmetic, which genuinely differs between the
|
||||
/// modes: it wraps at the width here and is exact rational arithmetic there.
|
||||
fn evalBinaryOp(config: Config, op: BinaryOp, left: u128, right: u128) CalcError!u128 {
|
||||
fn evalBinaryOp(config: Config, op: BinaryOp, left: u128, right: u128) Error!u128 {
|
||||
const mask = config.int_type.mask();
|
||||
|
||||
const result: u128 = switch (op) {
|
||||
|
|
@ -108,11 +119,11 @@ fn evalBinaryOp(config: Config, op: BinaryOp, left: u128, right: u128) CalcError
|
|||
.sub => (left -% right) & mask,
|
||||
.mul => (left *% right) & mask,
|
||||
.div => blk: {
|
||||
if (right == 0) return CalcError.DivisionByZero;
|
||||
if (right == 0) return Error.DivisionByZero;
|
||||
break :blk (left / right) & mask;
|
||||
},
|
||||
.mod => blk: {
|
||||
if (right == 0) return CalcError.DivisionByZero;
|
||||
if (right == 0) return Error.DivisionByZero;
|
||||
break :blk (left % right) & mask;
|
||||
},
|
||||
.pow => blk: {
|
||||
|
|
@ -141,7 +152,7 @@ fn evalBinaryOp(config: Config, op: BinaryOp, left: u128, right: u128) CalcError
|
|||
}
|
||||
|
||||
/// High-level: parse and evaluate a string in programmer mode.
|
||||
pub fn evalProgrammerString(allocator: Allocator, source: []const u8, config: Config) CalcError!Integer {
|
||||
pub fn evalProgrammerString(allocator: Allocator, source: []const u8, config: Config) Error!Integer {
|
||||
var p = Parser.init(allocator, source);
|
||||
const expr = try p.parse();
|
||||
// Same ownership rule as evalStringInfo: the tree is ours to release, and the
|
||||
|
|
@ -211,7 +222,7 @@ test "prog: division" {
|
|||
|
||||
test "prog: division by zero" {
|
||||
const result = testProg("10 / 0");
|
||||
try testing.expectError(CalcError.DivisionByZero, result);
|
||||
try testing.expectError(Error.DivisionByZero, result);
|
||||
}
|
||||
|
||||
test "prog: modulo" {
|
||||
|
|
@ -369,21 +380,21 @@ test "prog: variable reference errors" {
|
|||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer _ = arena.deinit();
|
||||
const result = evalProgrammerString(arena.allocator(), "x", .{});
|
||||
try testing.expectError(CalcError.UnknownVariable, result);
|
||||
try testing.expectError(Error.UnknownVariable, result);
|
||||
}
|
||||
|
||||
test "prog: assignment errors" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer _ = arena.deinit();
|
||||
const result = evalProgrammerString(arena.allocator(), "X = 5", .{});
|
||||
try testing.expectError(CalcError.InvalidOperandType, result);
|
||||
try testing.expectError(Error.InvalidOperandType, result);
|
||||
}
|
||||
|
||||
test "prog: function call errors" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer _ = arena.deinit();
|
||||
const result = evalProgrammerString(arena.allocator(), "sin(1)", .{});
|
||||
try testing.expectError(CalcError.UnknownFunction, result);
|
||||
try testing.expectError(Error.UnknownFunction, result);
|
||||
}
|
||||
|
||||
test "prog: ASCII literal single char" {
|
||||
|
|
@ -411,7 +422,7 @@ test "prog: ASCII literal overflow 8-bit" {
|
|||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer _ = arena.deinit();
|
||||
const result = evalProgrammerString(arena.allocator(), "'AB'", .{ .int_type = .{ .width = .bits8 } });
|
||||
try testing.expectError(CalcError.Overflow, result);
|
||||
try testing.expectError(Error.Overflow, result);
|
||||
}
|
||||
|
||||
test "prog: float literal truncates to integer" {
|
||||
|
|
@ -465,11 +476,11 @@ test "programmer mode: a float literal out of range errors instead of aborting"
|
|||
// `tally -p '1e40'` used to abort the process here: @intFromFloat on a value
|
||||
// past u128 is illegal behaviour, and only 3.14 was ever tested.
|
||||
try std.testing.expectError(
|
||||
CalcError.Overflow,
|
||||
Error.Overflow,
|
||||
evalProgrammerString(std.testing.allocator, "1e40", config),
|
||||
);
|
||||
try std.testing.expectError(
|
||||
CalcError.Overflow,
|
||||
Error.Overflow,
|
||||
evalProgrammerString(std.testing.allocator, "1e100", config),
|
||||
);
|
||||
// Still truncates the values that do fit.
|
||||
|
|
@ -483,7 +494,7 @@ test "programmer mode: an infinite or NaN literal is a domain error" {
|
|||
const config: Config = .{};
|
||||
// 10^400 overflows the exact tier's float projection to infinity.
|
||||
try std.testing.expectError(
|
||||
CalcError.DomainError,
|
||||
Error.DomainError,
|
||||
evalProgrammerString(std.testing.allocator, "1e400", config),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,19 @@
|
|||
//! No allocation, no I/O. Adding a unit means adding a table entry.
|
||||
|
||||
const std = @import("std");
|
||||
const CalcError = @import("errors.zig").CalcError;
|
||||
const rational_mod = @import("rational.zig");
|
||||
const Rational = rational_mod.Rational;
|
||||
/// What unit conversion can fail with: its own two errors, plus whatever the exact
|
||||
/// numeric path can raise, since the exact conversions compute in `Number`.
|
||||
///
|
||||
/// The `||` is the point: this set grows when `number.Error` grows, without anyone
|
||||
/// maintaining a list. It used to be one engine-wide set, and the translation from
|
||||
/// numeric errors into it (`mapNumberError`) was a second copy of the same mapping
|
||||
/// the evaluator had.
|
||||
pub const Error = error{
|
||||
UnknownUnit,
|
||||
IncompatibleUnits,
|
||||
OutOfMemory,
|
||||
} || number_mod.Error;
|
||||
const Rational = @import("Rational.zig");
|
||||
const number_mod = @import("number.zig");
|
||||
const Number = number_mod.Number;
|
||||
|
||||
|
|
@ -440,8 +450,8 @@ pub fn findUnit(name: []const u8) ?UnitDef {
|
|||
|
||||
/// Convert a value between two already-resolved units.
|
||||
/// Returns IncompatibleUnits if the units are in different categories.
|
||||
pub fn convertUnits(value: f64, from: UnitDef, to: UnitDef) CalcError!f64 {
|
||||
if (from.category != to.category) return CalcError.IncompatibleUnits;
|
||||
pub fn convertUnits(value: f64, from: UnitDef, to: UnitDef) Error!f64 {
|
||||
if (from.category != to.category) return Error.IncompatibleUnits;
|
||||
if (std.mem.eql(u8, from.name, to.name)) return value;
|
||||
return to.fromBase(from.toBase(value));
|
||||
}
|
||||
|
|
@ -449,9 +459,9 @@ pub fn convertUnits(value: f64, from: UnitDef, to: UnitDef) CalcError!f64 {
|
|||
/// Convert a value between two units named by string (canonical name or alias).
|
||||
/// Returns UnknownUnit if either name is unrecognized, or IncompatibleUnits if
|
||||
/// the units belong to different categories.
|
||||
pub fn convert(value: f64, from_name: []const u8, to_name: []const u8) CalcError!ConvertResult {
|
||||
const from = findUnit(from_name) orelse return CalcError.UnknownUnit;
|
||||
const to = findUnit(to_name) orelse return CalcError.UnknownUnit;
|
||||
pub fn convert(value: f64, from_name: []const u8, to_name: []const u8) Error!ConvertResult {
|
||||
const from = findUnit(from_name) orelse return Error.UnknownUnit;
|
||||
const to = findUnit(to_name) orelse return Error.UnknownUnit;
|
||||
const result = try convertUnits(value, from, to);
|
||||
|
||||
// A single scaling factor only describes the relationship when neither
|
||||
|
|
@ -570,14 +580,14 @@ fn splitTrailingUnit(text: []const u8) ?struct { value_text: []const u8, unit: U
|
|||
/// valid conversion. That is what disambiguates `in` the separator from `in` the
|
||||
/// unit: in "5 in in cm" the later `in` is the separator, while in "100 mm in in"
|
||||
/// the earlier one is, because only that reading resolves.
|
||||
pub fn parseRequest(text: []const u8) CalcError!?ConversionRequest {
|
||||
pub fn parseRequest(text: []const u8) Error!?ConversionRequest {
|
||||
var separators: [max_separators]Span = undefined;
|
||||
const count = collectSeparators(text, &separators);
|
||||
if (count == 0) return null;
|
||||
|
||||
// Remember why the most recent candidate failed, so a committed-but-invalid
|
||||
// request reports a useful error instead of a parse error.
|
||||
var failure: ?CalcError = null;
|
||||
var failure: ?Error = null;
|
||||
|
||||
var idx = count;
|
||||
while (idx > 0) {
|
||||
|
|
@ -589,15 +599,15 @@ pub fn parseRequest(text: []const u8) CalcError!?ConversionRequest {
|
|||
if (left.len == 0 or right.len == 0) continue;
|
||||
|
||||
const to_unit = findUnit(right) orelse {
|
||||
failure = CalcError.UnknownUnit;
|
||||
failure = Error.UnknownUnit;
|
||||
continue;
|
||||
};
|
||||
const split = splitTrailingUnit(left) orelse {
|
||||
failure = CalcError.UnknownUnit;
|
||||
failure = Error.UnknownUnit;
|
||||
continue;
|
||||
};
|
||||
if (split.unit.category != to_unit.category) {
|
||||
failure = CalcError.IncompatibleUnits;
|
||||
failure = Error.IncompatibleUnits;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -623,10 +633,10 @@ pub fn convertExactUnits(
|
|||
value: Number,
|
||||
from: UnitDef,
|
||||
to: UnitDef,
|
||||
) CalcError!Number {
|
||||
if (from.category != to.category) return CalcError.IncompatibleUnits;
|
||||
) Error!Number {
|
||||
if (from.category != to.category) return Error.IncompatibleUnits;
|
||||
if (std.mem.eql(u8, from.name, to.name)) {
|
||||
return value.cloneWith(allocator) catch |err| return mapNumberError(err);
|
||||
return try value.cloneWith(allocator);
|
||||
}
|
||||
|
||||
// No exact factor available, or the value is already inexact: use floats.
|
||||
|
|
@ -635,7 +645,7 @@ pub fn convertExactUnits(
|
|||
return Number.fromFloat(converted);
|
||||
}
|
||||
|
||||
return convertExactInner(allocator, value, from, to) catch |err| mapNumberError(err);
|
||||
return convertExactInner(allocator, value, from, to);
|
||||
}
|
||||
|
||||
fn convertExactInner(
|
||||
|
|
@ -666,9 +676,6 @@ fn convertExactInner(
|
|||
return Number.div(allocator, shifted, to_factor);
|
||||
}
|
||||
|
||||
/// One mapping, in `number.zig`; this alias keeps the call sites short.
|
||||
const mapNumberError = number_mod.toCalcError;
|
||||
|
||||
// -- Tests --
|
||||
|
||||
const testing = std.testing;
|
||||
|
|
@ -956,16 +963,16 @@ test "findUnit returns null for unknown names" {
|
|||
}
|
||||
|
||||
test "convert: unknown source unit errors" {
|
||||
try testing.expectError(CalcError.UnknownUnit, convert(1, "bogus", "m"));
|
||||
try testing.expectError(Error.UnknownUnit, convert(1, "bogus", "m"));
|
||||
}
|
||||
|
||||
test "convert: unknown target unit errors" {
|
||||
try testing.expectError(CalcError.UnknownUnit, convert(1, "m", "bogus"));
|
||||
try testing.expectError(Error.UnknownUnit, convert(1, "m", "bogus"));
|
||||
}
|
||||
|
||||
test "convert: incompatible categories error" {
|
||||
try testing.expectError(CalcError.IncompatibleUnits, convert(1, "kg", "m"));
|
||||
try testing.expectError(CalcError.IncompatibleUnits, convert(1, "C", "s"));
|
||||
try testing.expectError(Error.IncompatibleUnits, convert(1, "kg", "m"));
|
||||
try testing.expectError(Error.IncompatibleUnits, convert(1, "C", "s"));
|
||||
}
|
||||
|
||||
test "convert: same unit is identity" {
|
||||
|
|
@ -1146,21 +1153,21 @@ test "parseRequest: 'to' inside a word is not a keyword" {
|
|||
}
|
||||
|
||||
test "parseRequest: unknown target unit errors" {
|
||||
try testing.expectError(CalcError.UnknownUnit, parseRequest("100 km to smoots"));
|
||||
try testing.expectError(Error.UnknownUnit, parseRequest("100 km to smoots"));
|
||||
}
|
||||
|
||||
test "parseRequest: unknown source unit errors" {
|
||||
try testing.expectError(CalcError.UnknownUnit, parseRequest("100 smoots to km"));
|
||||
try testing.expectError(Error.UnknownUnit, parseRequest("100 smoots to km"));
|
||||
}
|
||||
|
||||
test "parseRequest: missing value errors" {
|
||||
// "km to mi" has a unit with no value in front of it
|
||||
try testing.expectError(CalcError.UnknownUnit, parseRequest("km to mi"));
|
||||
try testing.expectError(Error.UnknownUnit, parseRequest("km to mi"));
|
||||
}
|
||||
|
||||
test "parseRequest: mismatched categories error" {
|
||||
try testing.expectError(CalcError.IncompatibleUnits, parseRequest("1 kg to m"));
|
||||
try testing.expectError(CalcError.IncompatibleUnits, parseRequest("32F to km"));
|
||||
try testing.expectError(Error.IncompatibleUnits, parseRequest("1 kg to m"));
|
||||
try testing.expectError(Error.IncompatibleUnits, parseRequest("32F to km"));
|
||||
}
|
||||
|
||||
test "parseRequest: dangling keyword returns null" {
|
||||
|
|
@ -1176,8 +1183,8 @@ test "parseRequest: result feeds convertUnits correctly" {
|
|||
|
||||
test "parseRequest: does not find a unit buried inside a word" {
|
||||
// The trailing "s" of "smoots" must not be read as seconds.
|
||||
try testing.expectError(CalcError.UnknownUnit, parseRequest("100 smoots to km"));
|
||||
try testing.expectError(CalcError.UnknownUnit, parseRequest("5 bananas to kg"));
|
||||
try testing.expectError(Error.UnknownUnit, parseRequest("100 smoots to km"));
|
||||
try testing.expectError(Error.UnknownUnit, parseRequest("5 bananas to kg"));
|
||||
}
|
||||
|
||||
test "parseRequest: glued digit boundary still works" {
|
||||
|
|
@ -1240,8 +1247,8 @@ test "parseRequest: trailing whitespace after the separator still resolves" {
|
|||
}
|
||||
|
||||
test "parseRequest: backtracking reports the useful error, not a parse error" {
|
||||
try testing.expectError(CalcError.UnknownUnit, parseRequest("5 cm in smoots"));
|
||||
try testing.expectError(CalcError.IncompatibleUnits, parseRequest("5 cm in kg"));
|
||||
try testing.expectError(Error.UnknownUnit, parseRequest("5 cm in smoots"));
|
||||
try testing.expectError(Error.IncompatibleUnits, parseRequest("5 cm in kg"));
|
||||
}
|
||||
|
||||
// -- Exact conversion (Task 2.0e) --
|
||||
|
|
@ -1360,7 +1367,7 @@ test "exact: incompatible categories still error" {
|
|||
var value = try Number.parse(alloc, "1");
|
||||
defer value.deinit();
|
||||
try testing.expectError(
|
||||
CalcError.IncompatibleUnits,
|
||||
Error.IncompatibleUnits,
|
||||
convertExactUnits(alloc, value, findUnit("kg").?, findUnit("m").?),
|
||||
);
|
||||
}
|
||||
|
|
@ -1446,8 +1453,9 @@ test "exact: only pi-derived units lack an exact factor" {
|
|||
}
|
||||
|
||||
test "OOM safety: exact conversion releases everything at any failure point" {
|
||||
// Also the only realistic way to reach mapNumberError, which translates the
|
||||
// numeric model's errors into the engine's error set.
|
||||
// The exact path computes in `Number`, so `OutOfMemory` reaches the caller from
|
||||
// the numeric tier directly; `units.Error` includes `number.Error` for exactly
|
||||
// this reason and nothing translates between them.
|
||||
const alloc = testing.allocator;
|
||||
const from = findUnit("in").?;
|
||||
const to = findUnit("ft").?;
|
||||
|
|
@ -1458,14 +1466,14 @@ test "OOM safety: exact conversion releases everything at any failure point" {
|
|||
const a = failing.allocator();
|
||||
|
||||
var value = Number.parse(a, "12") catch |err| {
|
||||
try testing.expectEqual(rational_mod.Error.OutOfMemory, err);
|
||||
try testing.expectEqual(Rational.Error.OutOfMemory, err);
|
||||
continue;
|
||||
};
|
||||
defer value.deinit();
|
||||
|
||||
var result = convertExactUnits(a, value, from, to) catch |err| {
|
||||
// The numeric model's OutOfMemory must surface as the engine's.
|
||||
try testing.expectEqual(CalcError.OutOfMemory, err);
|
||||
try testing.expectEqual(Error.OutOfMemory, err);
|
||||
continue;
|
||||
};
|
||||
result.deinit();
|
||||
|
|
|
|||
22
src/main.zig
22
src/main.zig
|
|
@ -363,9 +363,9 @@ pub fn formatConversion(
|
|||
to_name: []const u8,
|
||||
) CliResult {
|
||||
const from = engine.units.findUnit(from_name) orelse
|
||||
return .{ .output = errorMessage(engine.CalcError.UnknownUnit), .is_error = true };
|
||||
return .{ .output = errorMessage(engine.Error.UnknownUnit), .is_error = true };
|
||||
const to = engine.units.findUnit(to_name) orelse
|
||||
return .{ .output = errorMessage(engine.CalcError.UnknownUnit), .is_error = true };
|
||||
return .{ .output = errorMessage(engine.Error.UnknownUnit), .is_error = true };
|
||||
|
||||
// Parse the value exactly rather than through f64, so a decimal input like
|
||||
// 2.5 enters the conversion without being rounded first.
|
||||
|
|
@ -540,23 +540,23 @@ fn formatProgrammerResult(buf: []u8, result: engine.Integer, config: engine.prog
|
|||
|
||||
/// Turn an engine error into a CLI line.
|
||||
///
|
||||
/// The phrases live once, in `engine.errors.errorPhrase`. This adds the prefix and
|
||||
/// The phrases live once, in `engine.phrase`. This adds the prefix and
|
||||
/// the newline at comptime, so the strings still have static lifetime and there is
|
||||
/// no second copy of the wording to drift. The CLI and the TUI previously each kept
|
||||
/// their own switch over the whole error set; the TUI's was already missing three
|
||||
/// cases and rendered them as "evaluation error".
|
||||
fn errorMessage(err: engine.CalcError) []const u8 {
|
||||
fn errorMessage(err: engine.Error) []const u8 {
|
||||
return decoratedError(err);
|
||||
}
|
||||
|
||||
/// Comptime-decorated form of every error phrase: "error: <phrase>\n".
|
||||
///
|
||||
/// `inline else` makes this exhaustive over the error set with no fallback branch:
|
||||
/// a new `CalcError` member is a compile error in `errorPhrase`, not a string that
|
||||
/// silently reads "evaluation error".
|
||||
fn decoratedError(err: engine.CalcError) []const u8 {
|
||||
/// `inline else` makes this exhaustive over the error set with no fallback branch: an
|
||||
/// error added to any engine module fails to compile in `message.phrase` rather than
|
||||
/// silently reading "evaluation error" here.
|
||||
fn decoratedError(err: engine.Error) []const u8 {
|
||||
return switch (err) {
|
||||
inline else => |e| comptime "error: " ++ engine.errors.errorPhrase(e) ++ "\n",
|
||||
inline else => |e| comptime "error: " ++ engine.phrase(e) ++ "\n",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -766,9 +766,9 @@ fn oomResult() CliResult {
|
|||
return .{ .output = "error: out of memory\n", .is_error = true };
|
||||
}
|
||||
|
||||
fn amortErrorMessage(err: engine.CalcError) []const u8 {
|
||||
fn amortErrorMessage(err: engine.Error) []const u8 {
|
||||
return switch (err) {
|
||||
engine.CalcError.DomainError =>
|
||||
engine.Error.DomainError =>
|
||||
// The realistic causes are all one of these, and a bare "domain error"
|
||||
// would leave the user guessing which.
|
||||
"error: check the loan terms: principal and periods must be positive, the rate cannot be negative, and the payment must at least cover the first period's interest\n",
|
||||
|
|
|
|||
32
src/tui.zig
32
src/tui.zig
|
|
@ -1407,13 +1407,13 @@ pub fn drawHistory(items: []const App.HistoryEntry, surface: *vxfw.Surface, star
|
|||
|
||||
/// Turn an engine error into a status-line string.
|
||||
///
|
||||
/// The phrases live once, in `engine.errors.errorPhrase`; the prefix is added at
|
||||
/// The phrases live once, in `engine.phrase`; the prefix is added at
|
||||
/// comptime. This switch used to be a second full copy of the CLI's, and had fallen
|
||||
/// behind: `InsufficientParameters`, `ConvergenceFailure` and `InvalidExpression`
|
||||
/// all came out as "evaluation error".
|
||||
fn errorStr(err: engine.CalcError) []const u8 {
|
||||
fn errorStr(err: engine.Error) []const u8 {
|
||||
return switch (err) {
|
||||
inline else => |e| comptime "error: " ++ engine.errors.errorPhrase(e),
|
||||
inline else => |e| comptime "error: " ++ engine.phrase(e),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -2987,19 +2987,19 @@ test "typed characters reach the prompt and Ctrl-C quits" {
|
|||
}
|
||||
|
||||
test "errorStr covers the errors the TUI can surface" {
|
||||
const errors = [_]engine.CalcError{
|
||||
engine.CalcError.DivisionByZero,
|
||||
engine.CalcError.UnknownFunction,
|
||||
engine.CalcError.UnknownVariable,
|
||||
engine.CalcError.UnmatchedParen,
|
||||
engine.CalcError.UnexpectedToken,
|
||||
engine.CalcError.UnexpectedEnd,
|
||||
engine.CalcError.InvalidNumber,
|
||||
engine.CalcError.DomainError,
|
||||
engine.CalcError.Overflow,
|
||||
engine.CalcError.UnknownUnit,
|
||||
engine.CalcError.IncompatibleUnits,
|
||||
engine.CalcError.ConvergenceFailure,
|
||||
const errors = [_]engine.Error{
|
||||
engine.Error.DivisionByZero,
|
||||
engine.Error.UnknownFunction,
|
||||
engine.Error.UnknownVariable,
|
||||
engine.Error.UnmatchedParen,
|
||||
engine.Error.UnexpectedToken,
|
||||
engine.Error.UnexpectedEnd,
|
||||
engine.Error.InvalidNumber,
|
||||
engine.Error.DomainError,
|
||||
engine.Error.Overflow,
|
||||
engine.Error.UnknownUnit,
|
||||
engine.Error.IncompatibleUnits,
|
||||
engine.Error.ConvergenceFailure,
|
||||
};
|
||||
for (errors) |err| {
|
||||
const text = errorStr(err);
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ pub const Field = struct {
|
|||
pub const Outcome = union(enum) {
|
||||
/// The form is not yet answerable; the text says what it needs.
|
||||
hint: []const u8,
|
||||
err: engine.CalcError,
|
||||
err: engine.Error,
|
||||
value: Value,
|
||||
schedule: Schedule,
|
||||
|
||||
|
|
@ -870,15 +870,15 @@ fn money(buf: []u8, value: f64) []const u8 {
|
|||
/// Error text for this view.
|
||||
///
|
||||
/// Only the cases where a form knows more than the engine does are overridden; the
|
||||
/// rest defer to `engine.errors.errorPhrase`, so this is no longer a third copy of
|
||||
/// rest defer to `engine.phrase`, so this is no longer a third copy of
|
||||
/// the whole table. A generic "domain error" is useless in a form, where the cause
|
||||
/// is always one of a few bad entries, but "division by zero" needs no improving.
|
||||
pub fn errorText(err: engine.CalcError) []const u8 {
|
||||
pub fn errorText(err: engine.Error) []const u8 {
|
||||
return switch (err) {
|
||||
engine.CalcError.DomainError => "check the entries: values must be positive and a payment must cover the interest",
|
||||
engine.CalcError.ConvergenceFailure => "no rate solves these cash flows",
|
||||
engine.CalcError.InsufficientParameters => "these values do not determine an answer",
|
||||
else => engine.errors.errorPhrase(err),
|
||||
engine.Error.DomainError => "check the entries: values must be positive and a payment must cover the interest",
|
||||
engine.Error.ConvergenceFailure => "no rate solves these cash flows",
|
||||
engine.Error.InsufficientParameters => "these values do not determine an answer",
|
||||
else => engine.phrase(err),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1103,7 +1103,7 @@ test "cagr form: says what it is waiting for" {
|
|||
|
||||
test "cagr form: a bad entry is an error, not a wrong answer" {
|
||||
var state = stateWith(.cagr, &.{ "0", "25000", "5" });
|
||||
try testing.expectEqual(engine.CalcError.DomainError, state.outcome().err);
|
||||
try testing.expectEqual(engine.Error.DomainError, state.outcome().err);
|
||||
}
|
||||
|
||||
test "compound form: solves whichever of the four variables is blank" {
|
||||
|
|
@ -1370,19 +1370,19 @@ test "every form has a label, a formula, and at most max_fields fields" {
|
|||
}
|
||||
|
||||
test "errorText: every financial error has its own wording" {
|
||||
const errors = [_]engine.CalcError{
|
||||
engine.CalcError.DomainError,
|
||||
engine.CalcError.ConvergenceFailure,
|
||||
engine.CalcError.InsufficientParameters,
|
||||
engine.CalcError.DivisionByZero,
|
||||
engine.CalcError.Overflow,
|
||||
engine.CalcError.UnknownUnit,
|
||||
const errors = [_]engine.Error{
|
||||
engine.Error.DomainError,
|
||||
engine.Error.ConvergenceFailure,
|
||||
engine.Error.InsufficientParameters,
|
||||
engine.Error.DivisionByZero,
|
||||
engine.Error.Overflow,
|
||||
engine.Error.UnknownUnit,
|
||||
};
|
||||
for (errors) |err| try testing.expect(errorText(err).len > 0);
|
||||
try testing.expect(!std.mem.eql(
|
||||
u8,
|
||||
errorText(engine.CalcError.DomainError),
|
||||
errorText(engine.CalcError.ConvergenceFailure),
|
||||
errorText(engine.Error.DomainError),
|
||||
errorText(engine.Error.ConvergenceFailure),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue