182 lines
7.9 KiB
Zig
182 lines
7.9 KiB
Zig
//! Tally calculation engine.
|
|
//!
|
|
//! Pure computation library with no I/O. Provides expression parsing, evaluation,
|
|
//! programmer-mode bit manipulation, unit conversion, and financial calculations.
|
|
|
|
const std = @import("std");
|
|
|
|
// Vocabulary, lowest first.
|
|
pub const grouping = @import("grouping.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 number = @import("number.zig");
|
|
// Language layer.
|
|
pub const tokenizer = @import("tokenizer.zig");
|
|
pub const ast = @import("ast.zig");
|
|
pub const parser = @import("parser.zig");
|
|
pub const evaluator = @import("evaluator.zig");
|
|
// Fixed-width integer operations, shared by both modes.
|
|
pub const bitwise = @import("bitwise.zig");
|
|
pub const programmer = @import("programmer.zig");
|
|
// Domains and display.
|
|
pub const float_interp = @import("float_interp.zig");
|
|
pub const units = @import("units.zig");
|
|
pub const financial = @import("financial.zig");
|
|
|
|
// The modules above are the engine's surface: a caller writes `engine.units.convert`
|
|
// or `engine.financial.solveTvm`. The aliases below exist only for the handful of
|
|
// names used often enough that the module prefix is noise. There used to be a
|
|
// 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 BitWidth = Integer.BitWidth;
|
|
pub const Environment = evaluator.Environment;
|
|
pub const evalString = evaluator.evalString;
|
|
pub const evalStringInfo = evaluator.evalStringInfo;
|
|
pub const evalProgrammerString = programmer.evalProgrammerString;
|
|
pub const FloatFormat = float_interp.FloatFormat;
|
|
pub const UnitCategory = units.UnitCategory;
|
|
pub const UnitDef = units.UnitDef;
|
|
pub const Number = number.Number;
|
|
|
|
/// 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 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",
|
|
// A valid number with more digits than the exact parser will hold, which is
|
|
// not the same as a malformed one.
|
|
error.TooManyDigits => "too many digits",
|
|
|
|
// 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",
|
|
};
|
|
}
|
|
|
|
// -- 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),
|
|
);
|
|
}
|