//! 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"); // The engine's surface, lowest first: a caller writes `engine.units.convert` or // `engine.financial.solveTvm`. Every module here has an importer outside the engine. pub const grouping = @import("grouping.zig"); pub const Integer = @import("Integer.zig"); pub const evaluator = @import("evaluator.zig"); pub const programmer = @import("programmer.zig"); pub const float_interp = @import("float_interp.zig"); pub const units = @import("units.zig"); pub const financial = @import("financial.zig"); // Reached only from inside this file. `Rational`, `tokenizer`, `ast`, `bitwise` and // `Parser` were public too and no frontend imported any of them: a caller evaluates // with `evalString` rather than building a tree, and computes on `Number` rather than // on the rational tier underneath it. `refAllDecls` in the tests below analyses every // declaration, which is what kept them compiling and hid that nothing wanted them. // // They are still reachable by path (`@import("Rational.zig")`) for anything inside // the engine, and their tests still run: every one of them is imported by a module // above, so it is part of the compilation either way. const number = @import("number.zig"); const Parser = @import("Parser.zig"); // 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 previewStringInfo = evaluator.previewStringInfo; 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.ExpressionTooLarge => "the expression has too many terms", error.NestingTooDeep => "the expression is nested too deeply", error.UnterminatedString => "unterminated string literal", error.InvalidNumber => "invalid number", // Names error.UnknownFunction => "unknown function", error.WrongArgumentCount => "wrong number of arguments", error.UnknownVariable => "unknown variable", error.AssignmentToConstant => "cannot assign to a built-in constant", // 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.AmbiguousUnit => "ambiguous unit name: case tells these units apart", 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("the expression has too many terms", phrase(error.ExpressionTooLarge)); try testing.expectEqualStrings("no solution found", phrase(error.ConvergenceFailure)); try testing.expectEqualStrings( "these values do not determine an answer", phrase(error.InsufficientParameters), ); }