From cb7da0fe9ea64ce185fcab7455c6032fd27cec2d Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 30 Jul 2026 08:21:09 -0700 Subject: [PATCH] begin human review - rename Integer, remove types.zig --- .kiro/specs/calculator/design.md | 64 ++++++-- .kiro/specs/calculator/tasks.md | 44 +++++ engine/src/Integer.zig | 179 ++++++++++++++++++++ engine/src/ast.zig | 3 +- engine/src/bitwise.zig | 213 ++++++++++-------------- engine/src/engine.zig | 29 ++-- engine/src/errors.zig | 136 +++++++++++++++ engine/src/evaluator.zig | 83 +++++----- engine/src/financial.zig | 3 +- engine/src/formatter.zig | 63 ++++++- engine/src/number.zig | 4 +- engine/src/parser.zig | 3 +- engine/src/programmer.zig | 113 +++++++------ engine/src/tokenizer.zig | 12 +- engine/src/types.zig | 274 ------------------------------- engine/src/units.zig | 3 +- src/main.zig | 95 ++++++----- src/tui.zig | 111 +++++++------ src/tui/financial.zig | 4 +- src/tui/float_view.zig | 2 +- src/tui/programmer.zig | 8 +- 21 files changed, 800 insertions(+), 646 deletions(-) create mode 100644 engine/src/Integer.zig create mode 100644 engine/src/errors.zig delete mode 100644 engine/src/types.zig diff --git a/.kiro/specs/calculator/design.md b/.kiro/specs/calculator/design.md index f0aab2c..ebed67b 100644 --- a/.kiro/specs/calculator/design.md +++ b/.kiro/specs/calculator/design.md @@ -61,18 +61,29 @@ build.zig (workspace root) | Module | Responsibility | |--------|---------------| -| `parser.zig` | Tokenizer + Pratt parser -> AST | +| `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) | +| `tokenizer.zig` | Lexer, and `Base` for literals | | `ast.zig` | AST node definitions | +| `parser.zig` | Pratt parser -> AST | | `evaluator.zig` | Walk AST, produce results | -| `programmer.zig` | Integer operations, base conversion, bit manipulation | | `bitwise.zig` | The fixed-width operators (`& \| xor ~ << >> >>> rol ror`), shared by both modes | -| `struct_layout.zig` | Struct DSL parser, layout computation, ABI profiles | +| `programmer.zig` | Programmer-mode evaluation, its `Config`, wrapping arithmetic | +| `formatter.zig` | Display and clipboard strings for every base | +| `float_interp.zig` | IEEE 754 bit-level interpretation | | `units.zig` | Unit conversion tables and resolver | | `financial.zig` | CAGR, TVM, compound interest, amortization | -| `types.zig` | Shared types (Value, Error, etc.) | -| `engine.zig` | Public API surface (Zig-native) | +| `engine.zig` | Public API surface (Zig-native). Imports and re-exports only; defines nothing | | `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). + ### 2.2 Core Data Types ```zig @@ -212,23 +223,44 @@ The evaluator maintains an `Environment`: ```zig pub const Environment = struct { allocator: Allocator, - mode: Mode, /// Variables hold `Number`, so an assignment keeps whatever exactness its /// expression had: `X = 0.1` stores exactly one tenth. variables: std.StringHashMap(Number), - ans: ?Number, - programmer_config: ProgrammerConfig, -}; - -pub const ProgrammerConfig = struct { - bit_width: BitWidth = .bits64, - signedness: Signedness = .signed, - /// Big-endian by default so the HEX row reads as the number itself; see FR-2.8. - display_endian: Endianness = .big, + ans: Number, }; ``` -Standard mode evaluates to `Number`, the exact/inexact union of section 2.7. Programmer mode evaluates to `Integer` (exact, truncated to bit width). Financial functions compute in `f64` and enter the expression language as inexact `Number` values (section 5.6). +It holds no mode and no programmer configuration. It used to hold both and read +neither: the caller picks `evalString` or `evalProgrammerString`, so which evaluator +runs is decided at the call rather than by state. The TUI was writing a mode into +that field on every mode change and nothing ever consulted it. `Mode` has left the +engine entirely; each frontend has its own, and they differ (the CLI has two, the TUI +has four including Convert, and the engine had a `financial` member it could never +act on because financial calculations are ordinary functions in expressions). + +Programmer mode's configuration belongs to programmer mode: + +```zig +// programmer.zig +pub const Config = struct { + int_type: IntType = .{}, + /// Big-endian by default so the HEX row reads as the number itself; see FR-2.8. + display_endian: std.builtin.Endian = .big, +}; + +// Integer.zig: one concept, previously reassembled in three places +pub const IntType = struct { + width: BitWidth = .bits64, + signedness: Signedness = .signed, +}; +``` + +`Integer.zig` is TitleCase because the file is the type: its top-level fields are +`raw` and `int_type`, and `IntType`, `BitWidth` and `Signedness` are declared inside +it. Byte order is `std.builtin.Endian` rather than an engine enum of the same two +members. + +Standard mode evaluates to `Number`, the exact/inexact union of section 2.7. Programmer mode evaluates to `Integer`, which is a `u128` pattern plus the `IntType` that interprets it. Financial functions compute in `f64` and enter the expression language as inexact `Number` values (section 5.6). ### 2.6 Number Display Formatting diff --git a/.kiro/specs/calculator/tasks.md b/.kiro/specs/calculator/tasks.md index aeedb7d..47c2d26 100644 --- a/.kiro/specs/calculator/tasks.md +++ b/.kiro/specs/calculator/tasks.md @@ -756,6 +756,50 @@ 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.12: Break up types.zig + +`types.zig` was named after a language feature rather than a concept, so it +accumulated four unrelated groups with almost no overlap in who used them. Found by +asking, during review, why the file existed at all. Each group went to the module +that owns it and the file is gone: + +- `engine/src/errors.zig`: `CalcError` and `errorPhrase`, the most widely depended-on + thing in the old file (nine engine modules, all three frontend files). +- `engine/src/Integer.zig`: the fixed-width integer model, TitleCase because the file + is the type (`raw` and `int_type` are its top-level fields). It declares `IntType` + (width plus signedness), `BitWidth` and `Signedness`. `IntType` replaced three + shapes of the same pair: `Integer{raw, bit_width, signedness}`, + `ProgrammerConfig{bit_width, signedness, display_endian}` and the `Domain` added to + `bitwise.zig` during the operator unification. `Integer` is now a pattern plus its + `IntType`, and `bitwise.zig` takes an `IntType` directly. The engine's own + `Endianness` enum is gone in favour of `std.builtin.Endian`, which has the same two + members. +- `Base` into `tokenizer.zig`: a lexical property with two users, the lexer and the + AST. +- `ProgrammerConfig` into `programmer.zig` as `Config`, holding an `IntType` and the + one display preference. +- `Mode` deleted from the engine. The frontends each own theirs, and they differ: the + CLI has two members, the TUI four (including Convert), and the engine's had a + `financial` member it could never act on. + +Two more pieces of dead state fell out, both the same pattern as the `Parser` and +`Tokenizer` fields removed in the de-duplication pass, and both hidden because a +frontend was writing to them: + +- `Environment.mode`, assigned in `init` and never read. `src/tui.zig setMode` was + writing it on every mode change. +- `Environment.programmer_config`, dead as of the operator unification, when `~` + stopped being the one operator that read a configured width in standard mode. + `Environment.init` now takes only an allocator. + +`engine.zig` defines nothing and only re-exports, which is why the error set did not +move there: every engine module would have had to import the root to name its own +error type. + +- Verify: 942 tests pass, fmt and zlint clean, `integer.zig` and `bitwise.zig` at + 100% line coverage, CLI output unchanged across arithmetic, programmer flags, + conversion and the new shift semantics. + ### Task 5.11: CLI programmer-mode flags and subcommand help The CLI could reach programmer mode with `-p` but not configure it, so the width, diff --git a/engine/src/Integer.zig b/engine/src/Integer.zig new file mode 100644 index 0000000..9060993 --- /dev/null +++ b/engine/src/Integer.zig @@ -0,0 +1,179 @@ +//! A fixed-width integer: a two's complement bit pattern plus the type that says +//! how to read it. +//! +//! Programmer mode computes on patterns of a chosen width rather than on the exact +//! rationals `number.zig` provides, and standard mode drops into the same +//! representation for the bitwise operators (FR-2.12). This file is that +//! representation; `bitwise.zig` is the operations on it. +//! +//! There used to be three shapes of the same idea: `Integer{raw, bit_width, +//! signedness}`, `ProgrammerConfig{bit_width, signedness, display_endian}`, and a +//! `Domain{bit_width, signedness}` inside `bitwise.zig`. `IntType` is the one +//! concept each was carrying a copy of: a value pairs it with bits, and a +//! configuration pairs it with a display preference. + +const std = @import("std"); + +const Integer = @This(); + +/// The bit pattern. May carry bits above the width; every reader masks. +raw: u128, +/// How to read `raw`. +int_type: IntType, + +pub fn init(raw: u128, int_type: IntType) Integer { + return .{ .raw = raw, .int_type = int_type }; +} + +/// Apply the width mask, truncating to the configured width. +pub fn masked(self: Integer) u128 { + return self.raw & self.int_type.mask(); +} + +/// Interpret as a signed value, sign-extended from the width. +pub fn signedValue(self: Integer) i128 { + return self.int_type.signExtend(self.raw); +} + +/// Interpret as an unsigned value, which is just the mask. +pub fn unsignedValue(self: Integer) u128 { + return self.masked(); +} + +/// A fixed-width integer type: how wide, and whether the top bit is a sign. +/// +/// Everything that interprets a pattern needs exactly this pair, which is why it +/// was being reassembled in three places. The default is 64-bit signed, which is +/// also standard mode's fixed type. +pub const IntType = struct { + width: BitWidth = .bits64, + signedness: Signedness = .signed, + + pub fn mask(self: IntType) u128 { + return self.width.mask(); + } + + pub fn bits(self: IntType) u8 { + return self.width.bits(); + } + + /// The top bit's position, whether or not this type treats it as a sign. + pub fn topBit(self: IntType) u128 { + return @as(u128, 1) << @intCast(self.bits() - 1); + } + + /// True when the pattern denotes a negative number in this type. An unsigned + /// type has no negative values, so `>>` is a zero fill there and a large shift + /// distance is large rather than negative. + pub fn isNegative(self: IntType, value: u128) bool { + return self.signedness == .signed and (value & self.mask()) & self.topBit() != 0; + } + + /// Sign-extend the pattern to a full i128. + pub fn signExtend(self: IntType, value: u128) i128 { + const m = value & self.mask(); + if (self.isNegative(m)) return @bitCast(m | ~self.mask()); + return @intCast(m); + } +}; + +/// Configurable integer bit width. The tag is the bit count. +pub const BitWidth = enum(u8) { + bits8 = 8, + bits16 = 16, + bits32 = 32, + bits64 = 64, + bits128 = 128, + + /// All bits set within the width. Shifts a full mask down, because + /// `(1 << bits) - 1` overflows at 128. + pub fn mask(self: BitWidth) u128 { + const shift: u7 = @intCast(128 - @as(u16, self.bits())); + return @as(u128, std.math.maxInt(u128)) >> shift; + } + + /// Returns the number of bits as a plain integer. + pub fn bits(self: BitWidth) u8 { + return @intFromEnum(self); + } +}; + +/// Whether the top bit of a pattern is a sign. +pub const Signedness = enum { + signed, + unsigned, +}; + +// -- Tests -- + +const testing = std.testing; + +test "BitWidth.mask" { + try testing.expectEqual(@as(u128, 0xFF), BitWidth.bits8.mask()); + try testing.expectEqual(@as(u128, 0xFFFF), BitWidth.bits16.mask()); + try testing.expectEqual(@as(u128, 0xFFFF_FFFF), BitWidth.bits32.mask()); + try testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF), BitWidth.bits64.mask()); + try testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF), BitWidth.bits128.mask()); +} + +test "BitWidth.mask: exactly `bits` low bits are set, at every width" { + for (std.enums.values(BitWidth)) |bw| { + try testing.expectEqual(@as(u8, bw.bits()), @popCount(bw.mask())); + try testing.expectEqual(@as(u128, 1), bw.mask() & 1); + } +} + +test "IntType: signExtend reads the top bit only when the type is signed" { + const i8_type: IntType = .{ .width = .bits8, .signedness = .signed }; + const u8_type: IntType = .{ .width = .bits8, .signedness = .unsigned }; + + try testing.expectEqual(@as(i128, -1), i8_type.signExtend(0xFF)); + try testing.expectEqual(@as(i128, 255), u8_type.signExtend(0xFF)); + try testing.expectEqual(@as(i128, -128), i8_type.signExtend(0x80)); + try testing.expectEqual(@as(i128, 127), i8_type.signExtend(0x7F)); + + try testing.expect(i8_type.isNegative(0x80)); + try testing.expect(!u8_type.isNegative(0x80)); + + // A 128-bit type has no bits above the width to fill. + const i128_type: IntType = .{ .width = .bits128, .signedness = .signed }; + try testing.expectEqual(@as(i128, -1), i128_type.signExtend(i128_type.mask())); +} + +test "IntType: the default is 64-bit signed, which standard mode uses" { + const default: IntType = .{}; + try testing.expectEqual(BitWidth.bits64, default.width); + try testing.expectEqual(Signedness.signed, default.signedness); + try testing.expectEqual(@as(i128, -1), default.signExtend(default.mask())); +} + +test "bits above the width never reach an interpretation" { + const i8_type: IntType = .{ .width = .bits8 }; + // The high bits are noise from a wider computation. + try testing.expectEqual(@as(i128, -1), i8_type.signExtend(0xDEAD_00FF)); + try testing.expectEqual(@as(u128, 0xFF), init(0xDEAD_00FF, i8_type).unsignedValue()); +} + +test "signedValue" { + const i8_type: IntType = .{ .width = .bits8 }; + try testing.expectEqual(@as(i128, -1), init(0xFF, i8_type).signedValue()); + try testing.expectEqual(@as(i128, 127), init(0x7F, i8_type).signedValue()); + try testing.expectEqual(@as(i128, -128), init(0x80, i8_type).signedValue()); + + const i32_type: IntType = .{ .width = .bits32 }; + try testing.expectEqual(@as(i128, -1), init(0xFFFF_FFFF, i32_type).signedValue()); +} + +test "unsignedValue masks correctly" { + const u8_type: IntType = .{ .width = .bits8, .signedness = .unsigned }; + try testing.expectEqual(@as(u128, 0xFF), init(0x1FF, u8_type).unsignedValue()); +} + +test "the same bits read two ways, which is why the type travels with the value" { + const bits: u128 = 0xFF; + try testing.expectEqual(@as(i128, -1), init(bits, .{ .width = .bits8 }).signedValue()); + try testing.expectEqual( + @as(i128, 255), + init(bits, .{ .width = .bits8, .signedness = .unsigned }).signedValue(), + ); +} diff --git a/engine/src/ast.zig b/engine/src/ast.zig index c34c828..357e282 100644 --- a/engine/src/ast.zig +++ b/engine/src/ast.zig @@ -11,8 +11,7 @@ //! deallocation"), which is how both eval entry points ended up leaking the whole //! tree on every call. -const types = @import("types.zig"); -const Base = types.Base; +const Base = @import("tokenizer.zig").Base; /// A single expression node. pub const Expr = union(enum) { diff --git a/engine/src/bitwise.zig b/engine/src/bitwise.zig index 1e244d7..db7a36d 100644 --- a/engine/src/bitwise.zig +++ b/engine/src/bitwise.zig @@ -15,21 +15,21 @@ //! `~`. //! //! FR-2.12 promises that every operator means the same thing in both modes, so -//! there is one implementation, parameterised by a `Domain` (width plus +//! there is one implementation, parameterised by an `IntType` (width plus //! signedness). Standard mode is fixed at 64-bit signed; a different width is what //! programmer mode is for (FR-2.3). //! //! Values are carried as a `u128` holding the two's complement bit pattern masked -//! to the width, which is the same representation `types.Integer` uses. +//! to the width, which is the same representation `Integer` uses. const std = @import("std"); const ast = @import("ast.zig"); const BinaryOp = ast.BinaryOp; -const types = @import("types.zig"); -const BitWidth = types.BitWidth; -const Signedness = types.Signedness; -const ProgrammerConfig = types.ProgrammerConfig; -const CalcError = types.CalcError; +const Integer = @import("Integer.zig"); +const BitWidth = Integer.BitWidth; +const Signedness = Integer.Signedness; +const IntType = Integer.IntType; +const CalcError = @import("errors.zig").CalcError; /// The operators this module implements. /// @@ -67,48 +67,6 @@ pub fn fromBinaryOp(op: BinaryOp) ?Op { }; } -/// The integer type an operation is carried out in: how wide, and whether the top -/// bit is a sign. -pub const Domain = struct { - bit_width: BitWidth, - signedness: Signedness, - - /// Standard mode: 64-bit two's complement. Fixed, not configurable; the - /// programmer-mode width setting does not reach standard mode. - pub const standard: Domain = .{ .bit_width = .bits64, .signedness = .signed }; - - pub fn fromConfig(config: ProgrammerConfig) Domain { - return .{ .bit_width = config.bit_width, .signedness = config.signedness }; - } - - pub fn mask(self: Domain) u128 { - return self.bit_width.mask(); - } - - pub fn bits(self: Domain) u8 { - return self.bit_width.bits(); - } - - /// The sign bit position, whether or not this domain treats it as a sign. - pub fn topBit(self: Domain) u128 { - return @as(u128, 1) << @intCast(self.bits() - 1); - } - - /// True when the pattern denotes a negative number in this domain. An unsigned - /// domain has no negative values, so `>>` is a zero fill there and a large - /// shift distance is large rather than negative. - pub fn isNegative(self: Domain, value: u128) bool { - return self.signedness == .signed and (value & self.mask()) & self.topBit() != 0; - } - - /// Sign-extend the pattern to a full i128. - pub fn signExtend(self: Domain, value: u128) i128 { - const m = value & self.mask(); - if (self.isNegative(m)) return @bitCast(m | ~self.mask()); - return @intCast(m); - } -}; - /// How far a shift moves, once the distance has been checked. const Distance = union(enum) { /// Shorter than the width, so some bits survive. @@ -121,10 +79,10 @@ 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(domain: Domain, right: u128) CalcError!Distance { - if (domain.isNegative(right)) return CalcError.DomainError; - const value = right & domain.mask(); - if (value >= domain.bits()) return .past_width; +fn distance(int_type: IntType, right: u128) CalcError!Distance { + if (int_type.isNegative(right)) return CalcError.DomainError; + const value = right & int_type.mask(); + if (value >= int_type.bits()) return .past_width; return .{ .within = @intCast(value) }; } @@ -132,14 +90,14 @@ fn distance(domain: Domain, 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(domain: Domain, right: u128) CalcError!u7 { - if (domain.isNegative(right)) return CalcError.DomainError; - return @intCast((right & domain.mask()) % domain.bits()); +fn rotation(int_type: IntType, right: u128) CalcError!u7 { + if (int_type.isNegative(right)) return CalcError.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(domain: Domain, op: Op, left_in: u128, right_in: u128) CalcError!u128 { - const mask = domain.mask(); +pub fn apply(int_type: IntType, op: Op, left_in: u128, right_in: u128) CalcError!u128 { + const mask = int_type.mask(); const left = left_in & mask; const right = right_in & mask; @@ -149,59 +107,59 @@ pub fn apply(domain: Domain, op: Op, left_in: u128, right_in: u128) CalcError!u1 .bit_xor => left ^ right, // Bits shifted past the end of the width are discarded, not wrapped: // `0b1000 << 1` is 16. Wrapping is what `rol` and `ror` are for. - .shift_left => switch (try distance(domain, right)) { + .shift_left => switch (try distance(int_type, right)) { .past_width => 0, .within => |amt| (left << amt) & mask, }, - .shift_right_logical => switch (try distance(domain, right)) { + .shift_right_logical => switch (try distance(int_type, right)) { .past_width => 0, .within => |amt| (left >> amt) & mask, }, .shift_right => blk: { - const negative = domain.isNegative(left); - switch (try distance(domain, right)) { + const negative = int_type.isNegative(left); + switch (try distance(int_type, right)) { // Shifting a negative value all the way out leaves the sign fill, // which is every bit set; a non-negative one leaves zero. .past_width => break :blk if (negative) mask else 0, .within => |amt| { if (!negative) break :blk (left >> amt) & mask; // Shift in the signed domain so the sign bit is the fill. - const extended = domain.signExtend(left); + const extended = int_type.signExtend(left); break :blk @as(u128, @bitCast(extended >> amt)) & mask; }, } }, .rotate_left => blk: { - const amt = try rotation(domain, right); + const amt = try rotation(int_type, right); if (amt == 0) break :blk left; - const anti: u7 = @intCast(domain.bits() - amt); + const anti: u7 = @intCast(int_type.bits() - amt); break :blk ((left << amt) | (left >> anti)) & mask; }, .rotate_right => blk: { - const amt = try rotation(domain, right); + const amt = try rotation(int_type, right); if (amt == 0) break :blk left; - const anti: u7 = @intCast(domain.bits() - amt); + const anti: u7 = @intCast(int_type.bits() - amt); break :blk ((left >> amt) | (left << anti)) & mask; }, }; } /// Bitwise complement within the width. -pub fn not(domain: Domain, value: u128) u128 { - return ~value & domain.mask(); +pub fn not(int_type: IntType, value: u128) u128 { + return ~value & int_type.mask(); } /// Two's complement negation within the width. -pub fn negate(domain: Domain, value: u128) u128 { - return (~value +% 1) & domain.mask(); +pub fn negate(int_type: IntType, value: u128) u128 { + return (~value +% 1) & int_type.mask(); } // -- Tests -- const testing = std.testing; -const w8: Domain = .{ .bit_width = .bits8, .signedness = .signed }; -const u8_domain: Domain = .{ .bit_width = .bits8, .signedness = .unsigned }; +const i8_type: IntType = .{ .width = .bits8 }; +const u8_type: IntType = .{ .width = .bits8, .signedness = .unsigned }; test "fromBinaryOp: every fixed-width operator maps, no arithmetic one does" { // The compiler enforces the total mapping; this pins which side each lands on. @@ -219,14 +177,14 @@ test "fromBinaryOp: every fixed-width operator maps, no arithmetic one does" { test "arithmetic right shift fills with the sign bit" { // -8 in 8 bits is 0b1111_1000; one place right is 0b1111_1100, which is -4. - const result = try apply(w8, .shift_right, 0b1111_1000, 1); + const result = try apply(i8_type, .shift_right, 0b1111_1000, 1); try testing.expectEqual(@as(u128, 0b1111_1100), result); - try testing.expectEqual(@as(i128, -4), w8.signExtend(result)); + try testing.expectEqual(@as(i128, -4), i8_type.signExtend(result)); } test "logical right shift fills with zeros" { // The same bits, shifted the other way: 0b0111_1100 is 124. - const result = try apply(w8, .shift_right_logical, 0b1111_1000, 1); + const result = try apply(i8_type, .shift_right_logical, 0b1111_1000, 1); try testing.expectEqual(@as(u128, 124), result); } @@ -236,8 +194,8 @@ test "the two right shifts agree on non-negative values" { var amt: u128 = 0; while (amt < 8) : (amt += 1) { try testing.expectEqual( - try apply(w8, .shift_right, value, amt), - try apply(w8, .shift_right_logical, value, amt), + try apply(i8_type, .shift_right, value, amt), + try apply(i8_type, .shift_right_logical, value, amt), ); } } @@ -247,60 +205,60 @@ test "an unsigned domain has no sign to extend" { // 0xFF is 255 here, not -1, so the arithmetic shift is a zero fill too. The // old programmer-mode implementation looked at the top bit regardless of the // configured signedness and gave 0xFF. - try testing.expectEqual(@as(u128, 0x7F), try apply(u8_domain, .shift_right, 0xFF, 1)); - try testing.expectEqual(@as(u128, 0x7F), try apply(u8_domain, .shift_right_logical, 0xFF, 1)); + try testing.expectEqual(@as(u128, 0x7F), try apply(u8_type, .shift_right, 0xFF, 1)); + try testing.expectEqual(@as(u128, 0x7F), try apply(u8_type, .shift_right_logical, 0xFF, 1)); } test "shifts run to completion instead of wrapping or clamping the distance" { // Standard mode reduced the distance modulo the width, so `1 << 64` was 1; // programmer mode clamped it to width - 1, so 8-bit `0xFF >>> 20` was 1. - const standard_domain = Domain.standard; - try testing.expectEqual(@as(u128, 0), try apply(standard_domain, .shift_left, 1, 64)); - try testing.expectEqual(@as(u128, 0), try apply(standard_domain, .shift_left, 1, 1000)); - try testing.expectEqual(@as(u128, 0), try apply(w8, .shift_right_logical, 0xFF, 20)); - try testing.expectEqual(@as(u128, 0), try apply(w8, .shift_left, 0xFF, 8)); + const i64_type: IntType = .{}; + try testing.expectEqual(@as(u128, 0), try apply(i64_type, .shift_left, 1, 64)); + try testing.expectEqual(@as(u128, 0), try apply(i64_type, .shift_left, 1, 1000)); + try testing.expectEqual(@as(u128, 0), try apply(i8_type, .shift_right_logical, 0xFF, 20)); + try testing.expectEqual(@as(u128, 0), try apply(i8_type, .shift_left, 0xFF, 8)); // A negative value shifted all the way out is all sign bits, not zero. - try testing.expectEqual(@as(u128, 0xFF), try apply(w8, .shift_right, 0b1111_1000, 8)); - try testing.expectEqual(@as(u128, 0xFF), try apply(w8, .shift_right, 0b1111_1000, 100)); + try testing.expectEqual(@as(u128, 0xFF), try apply(i8_type, .shift_right, 0b1111_1000, 8)); + try testing.expectEqual(@as(u128, 0xFF), try apply(i8_type, .shift_right, 0b1111_1000, 100)); // A non-negative one is zero. - try testing.expectEqual(@as(u128, 0), try apply(w8, .shift_right, 0b0100_0000, 8)); + try testing.expectEqual(@as(u128, 0), try apply(i8_type, .shift_right, 0b0100_0000, 8)); } test "shifting by one less than the width still keeps a bit" { // The boundary the clamping rule used to hide. - try testing.expectEqual(@as(u128, 0b1000_0000), try apply(w8, .shift_left, 1, 7)); - try testing.expectEqual(@as(u128, 1), try apply(w8, .shift_right_logical, 0b1000_0000, 7)); - try testing.expectEqual(@as(u128, 0xFF), try apply(w8, .shift_right, 0b1000_0000, 7)); + try testing.expectEqual(@as(u128, 0b1000_0000), try apply(i8_type, .shift_left, 1, 7)); + try testing.expectEqual(@as(u128, 1), try apply(i8_type, .shift_right_logical, 0b1000_0000, 7)); + try testing.expectEqual(@as(u128, 0xFF), try apply(i8_type, .shift_right, 0b1000_0000, 7)); } 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(w8, .shift_left, 1, neg_one)); - try testing.expectError(CalcError.DomainError, apply(w8, .shift_right, 1, neg_one)); - try testing.expectError(CalcError.DomainError, apply(w8, .shift_right_logical, 1, neg_one)); - try testing.expectError(CalcError.DomainError, apply(w8, .rotate_left, 1, neg_one)); - try testing.expectError(CalcError.DomainError, apply(w8, .rotate_right, 1, neg_one)); + 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)); // The same pattern in an unsigned domain is 255, a distance past the width. - try testing.expectEqual(@as(u128, 0), try apply(u8_domain, .shift_left, 1, neg_one)); + try testing.expectEqual(@as(u128, 0), try apply(u8_type, .shift_left, 1, neg_one)); } test "rotation is cyclic and reduces the distance" { - try testing.expectEqual(@as(u128, 0b0000_0011), try apply(w8, .rotate_left, 0b1000_0001, 1)); - try testing.expectEqual(@as(u128, 0b1100_0000), try apply(w8, .rotate_right, 0b1000_0001, 1)); + try testing.expectEqual(@as(u128, 0b0000_0011), try apply(i8_type, .rotate_left, 0b1000_0001, 1)); + try testing.expectEqual(@as(u128, 0b1100_0000), try apply(i8_type, .rotate_right, 0b1000_0001, 1)); // Rotating by the width is the identity, and by width + 1 is by 1. - try testing.expectEqual(@as(u128, 0b1000_0001), try apply(w8, .rotate_left, 0b1000_0001, 8)); - try testing.expectEqual(@as(u128, 0b0000_0011), try apply(w8, .rotate_left, 0b1000_0001, 9)); + try testing.expectEqual(@as(u128, 0b1000_0001), try apply(i8_type, .rotate_left, 0b1000_0001, 8)); + try testing.expectEqual(@as(u128, 0b0000_0011), try apply(i8_type, .rotate_left, 0b1000_0001, 9)); } test "rotate left and rotate right are inverses at every distance and width" { for ([_]BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |bw| { - const domain: Domain = .{ .bit_width = bw, .signedness = .unsigned }; - const value: u128 = 0x1234_5678_9ABC_DEF0 & domain.mask(); + const int_type: IntType = .{ .width = bw, .signedness = .unsigned }; + const value: u128 = 0x1234_5678_9ABC_DEF0 & int_type.mask(); var amt: u128 = 0; - while (amt < domain.bits()) : (amt += 1) { - const there = try apply(domain, .rotate_left, value, amt); - const back = try apply(domain, .rotate_right, there, amt); + while (amt < int_type.bits()) : (amt += 1) { + const there = try apply(int_type, .rotate_left, value, amt); + const back = try apply(int_type, .rotate_right, there, amt); try testing.expectEqual(value, back); } } @@ -308,41 +266,42 @@ test "rotate left and rotate right are inverses at every distance and width" { test "results stay inside the width" { for ([_]BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |bw| { - const domain: Domain = .{ .bit_width = bw, .signedness = .signed }; - const all_ones = domain.mask(); + const int_type: IntType = .{ .width = bw, .signedness = .signed }; + const all_ones = int_type.mask(); inline for (@typeInfo(Op).@"enum".fields) |field| { const op = @field(Op, field.name); // 1 is a safe distance for the shifts and a legal operand for the rest. - const result = try apply(domain, op, all_ones, 1); - try testing.expectEqual(result, result & domain.mask()); + const result = try apply(int_type, op, all_ones, 1); + try testing.expectEqual(result, result & int_type.mask()); } } } test "not and negate stay inside the width" { - try testing.expectEqual(@as(u128, 0xFF), not(w8, 0)); - try testing.expectEqual(@as(u128, 0), not(w8, 0xFF)); - try testing.expectEqual(@as(u128, 0xFF), negate(w8, 1)); - try testing.expectEqual(@as(u128, 1), negate(w8, 0xFF)); + try testing.expectEqual(@as(u128, 0xFF), not(i8_type, 0)); + try testing.expectEqual(@as(u128, 0), not(i8_type, 0xFF)); + try testing.expectEqual(@as(u128, 0xFF), negate(i8_type, 1)); + try testing.expectEqual(@as(u128, 1), negate(i8_type, 0xFF)); // Negating the most negative value gives itself back, as two's complement does. - try testing.expectEqual(@as(u128, 0x80), negate(w8, 0x80)); + try testing.expectEqual(@as(u128, 0x80), negate(i8_type, 0x80)); } test "signExtend reads the top bit only when the domain is signed" { - try testing.expectEqual(@as(i128, -1), w8.signExtend(0xFF)); - try testing.expectEqual(@as(i128, 255), u8_domain.signExtend(0xFF)); - try testing.expectEqual(@as(i128, -128), w8.signExtend(0x80)); - try testing.expectEqual(@as(i128, 127), w8.signExtend(0x7F)); + try testing.expectEqual(@as(i128, -1), i8_type.signExtend(0xFF)); + try testing.expectEqual(@as(i128, 255), u8_type.signExtend(0xFF)); + try testing.expectEqual(@as(i128, -128), i8_type.signExtend(0x80)); + try testing.expectEqual(@as(i128, 127), i8_type.signExtend(0x7F)); // A 128-bit domain has no bits above the width to fill. - const w128: Domain = .{ .bit_width = .bits128, .signedness = .signed }; - try testing.expectEqual(@as(i128, -1), w128.signExtend(w128.mask())); + const i128_type: IntType = .{ .width = .bits128 }; + try testing.expectEqual(@as(i128, -1), i128_type.signExtend(i128_type.mask())); } -test "standard mode is 64-bit signed" { - try testing.expectEqual(BitWidth.bits64, Domain.standard.bit_width); - try testing.expectEqual(Signedness.signed, Domain.standard.signedness); +test "the default type, which standard mode uses, is 64-bit signed" { + const i64_type: IntType = .{}; + try testing.expectEqual(BitWidth.bits64, i64_type.width); + try testing.expectEqual(Signedness.signed, i64_type.signedness); // -8 >> 1 is -4 there, which is the case that used to differ between modes. - const minus_eight: u128 = @bitCast(@as(i128, -8) & @as(i128, @bitCast(Domain.standard.mask()))); - const shifted = try apply(Domain.standard, .shift_right, minus_eight, 1); - try testing.expectEqual(@as(i128, -4), Domain.standard.signExtend(shifted)); + const minus_eight: u128 = @bitCast(@as(i128, -8) & @as(i128, @bitCast(i64_type.mask()))); + const shifted = try apply(i64_type, .shift_right, minus_eight, 1); + try testing.expectEqual(@as(i128, -4), i64_type.signExtend(shifted)); } diff --git a/engine/src/engine.zig b/engine/src/engine.zig index 97c7fc8..1e6f3e8 100644 --- a/engine/src/engine.zig +++ b/engine/src/engine.zig @@ -1,23 +1,31 @@ //! Tally calculation engine. //! -//! Pure computation library with no I/O. Provides expression parsing, -//! evaluation, programmer-mode bit manipulation, struct layout computation, -//! unit conversion, and financial calculations. +//! 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. -pub const types = @import("types.zig"); +// 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 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"); -pub const programmer = @import("programmer.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 formatter = @import("formatter.zig"); pub const float_interp = @import("float_interp.zig"); pub const units = @import("units.zig"); pub const financial = @import("financial.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"); // 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 @@ -25,9 +33,8 @@ pub const number = @import("number.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 Mode = types.Mode; -pub const BitWidth = types.BitWidth; -pub const CalcError = types.CalcError; +pub const CalcError = errors.CalcError; +pub const BitWidth = Integer.BitWidth; pub const Environment = evaluator.Environment; pub const evalString = evaluator.evalString; pub const evalStringInfo = evaluator.evalStringInfo; diff --git a/engine/src/errors.zig b/engine/src/errors.zig new file mode 100644 index 0000000..62f4a44 --- /dev/null +++ b/engine/src/errors.zig @@ -0,0 +1,136 @@ +//! 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), + ); +} diff --git a/engine/src/evaluator.zig b/engine/src/evaluator.zig index 0462b59..17cf63f 100644 --- a/engine/src/evaluator.zig +++ b/engine/src/evaluator.zig @@ -11,10 +11,9 @@ const Allocator = std.mem.Allocator; const ast = @import("ast.zig"); const Expr = ast.Expr; const BinaryOp = ast.BinaryOp; -const types = @import("types.zig"); -const Mode = types.Mode; -const ProgrammerConfig = types.ProgrammerConfig; -const CalcError = types.CalcError; +const Integer = @import("Integer.zig"); +const IntType = Integer.IntType; +const CalcError = @import("errors.zig").CalcError; const parser_mod = @import("parser.zig"); const Parser = parser_mod.Parser; const number_mod = @import("number.zig"); @@ -22,23 +21,24 @@ const Number = number_mod.Number; const bitwise = @import("bitwise.zig"); const financial = @import("financial.zig"); -/// Evaluation environment holding variables, history, and config. +/// Evaluation environment holding variables and the last answer. /// /// Variables and `Ans` are stored as `Number`, so an assignment keeps whatever /// exactness its expression had: `X = 0.1` stores exactly one tenth rather than /// a binary approximation of it. +/// +/// It holds no mode and no programmer configuration. It used to hold both and read +/// neither: the caller chooses between `evalString` and `evalProgrammerString`, and +/// standard mode's integer type is fixed (`standard_int_type`). The TUI was writing +/// a mode into this on every mode change, into a field nothing consulted. pub const Environment = struct { allocator: Allocator, - mode: Mode, - programmer_config: ProgrammerConfig, variables: std.StringHashMap(Number), ans: Number, - pub fn init(allocator: Allocator, mode: Mode) Environment { + pub fn init(allocator: Allocator) Environment { return .{ .allocator = allocator, - .mode = mode, - .programmer_config = .{}, .variables = std.StringHashMap(Number).init(allocator), // Starts inexact so that `init` cannot fail; the first evaluation // replaces it. @@ -173,7 +173,7 @@ fn evalExact(env: *Environment, scratch: Allocator, expr: *const Expr) CalcError // setting entirely. .bitwise_not => blk: { const bits = try toFixedWidthBits(operand.toFloat(scratch)); - break :blk fromFixedWidthBits(bitwise.not(standard_domain, bits)); + break :blk fromFixedWidthBits(bitwise.not(standard_int_type, bits)); }, }; }, @@ -232,7 +232,7 @@ fn evalBinaryOp(scratch: Allocator, op: BinaryOp, left: Number, right: Number) C const l = try toFixedWidthBits(left.toFloat(scratch)); const r = try toFixedWidthBits(right.toFloat(scratch)); const result = try bitwise.apply( - standard_domain, + standard_int_type, comptime bitwise.fromBinaryOp(fixed_op).?, l, r, @@ -242,12 +242,13 @@ fn evalBinaryOp(scratch: Allocator, op: BinaryOp, left: Number, right: Number) C }; } -/// Standard mode's integer domain: 64-bit two's complement, fixed (FR-2.3). +/// Standard mode's integer type: 64-bit two's complement, fixed (FR-2.3), which is +/// also `IntType`'s default. /// /// Standard mode does not consult the programmer-mode width. A width other than 64 /// is what programmer mode is for, and pretending otherwise is how `~` came to /// honour the setting while the shifts beside it did not. -const standard_domain = bitwise.Domain.standard; +const standard_int_type: IntType = .{}; /// Project a float onto the integer domain the bitwise operators work in. /// @@ -269,7 +270,7 @@ fn toFixedWidthBits(value: f64) CalcError!u128 { /// Read a result pattern back as a number, signed, since standard mode is signed. fn fromFixedWidthBits(bits: u128) Number { - return Number.fromFloat(@floatFromInt(standard_domain.signExtend(bits))); + return Number.fromFloat(@floatFromInt(standard_int_type.signExtend(bits))); } /// Evaluate a built-in function call. @@ -585,7 +586,7 @@ fn testEval(source: []const u8) !f64 { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); - var env = Environment.init(alloc, .standard); + var env = Environment.init(alloc); defer env.deinit(); const result = try evalString(&env, alloc, source); return result.toFloat(alloc); @@ -595,7 +596,7 @@ fn testEvalProgrammer(source: []const u8) !f64 { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); - var env = Environment.init(alloc, .programmer); + var env = Environment.init(alloc); defer env.deinit(); const result = try evalString(&env, alloc, source); return result.toFloat(alloc); @@ -765,7 +766,7 @@ test "eval variable assignment and use" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); - var env = Environment.init(alloc, .standard); + var env = Environment.init(alloc); defer env.deinit(); const assign_result = try evalString(&env, alloc, "X = 42"); @@ -779,7 +780,7 @@ test "eval Ans" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); - var env = Environment.init(alloc, .standard); + var env = Environment.init(alloc); defer env.deinit(); _ = try evalString(&env, alloc, "7 * 6"); @@ -873,8 +874,7 @@ test "standard mode: the fixed-width operators use one implementation with progr for (shared) |source| { const standard = try testEval(source); const prog = try programmer_mod.evalProgrammerString(alloc, source, .{ - .bit_width = .bits64, - .signedness = .signed, + .int_type = .{ .width = .bits64, .signedness = .signed }, }); try testing.expectEqual(@as(i128, @intFromFloat(standard)), prog.signedValue()); } @@ -918,13 +918,13 @@ test "standard mode: rotation is cyclic, not clamped" { try testing.expectEqual(@as(f64, 1.0), try testEval("1 ror 64")); } -test "standard mode: the programmer width setting does not reach it" { - // `~` used to read env.programmer_config.bit_width while the shifts beside it - // ignored it, so `--bits 8` changed one and not the other. Standard mode is - // fixed at 64-bit signed. - var env = Environment.init(testing.allocator, .standard); +test "standard mode: its integer type is fixed at 64-bit signed" { + // `~` used to read `env.programmer_config.bit_width` while the shifts beside it + // ignored it, so `--bits 8` changed one operator and not the others. The + // environment no longer carries a width to disagree about: there is nothing to + // set here, which is the point. + var env = Environment.init(testing.allocator); defer env.deinit(); - env.programmer_config.bit_width = .bits8; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); @@ -937,6 +937,9 @@ test "standard mode: the programmer width setting does not reach it" { var shifted = try evalString(&env, alloc, "1 << 10"); defer shifted.deinit(); try testing.expectEqual(@as(f64, 1024.0), shifted.toFloat(alloc)); + + try testing.expectEqual(@as(u8, 64), standard_int_type.bits()); + try testing.expectEqual(Integer.Signedness.signed, standard_int_type.signedness); } test "eval rotate left in standard mode" { @@ -1007,7 +1010,7 @@ test "evalStringInfo: detects hex literal" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); - var env = Environment.init(alloc, .standard); + var env = Environment.init(alloc); defer env.deinit(); const info = try evalStringInfo(&env, alloc, "0o777 - 0x0f"); try testing.expectEqual(@as(f64, 496.0), info.value.toFloat(alloc)); @@ -1018,7 +1021,7 @@ test "evalStringInfo: pure decimal has no nondecimal literal" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); - var env = Environment.init(alloc, .standard); + var env = Environment.init(alloc); defer env.deinit(); const info = try evalStringInfo(&env, alloc, "2 + 2"); try testing.expectEqual(@as(f64, 4.0), info.value.toFloat(alloc)); @@ -1029,7 +1032,7 @@ test "evalStringInfo: binary literal detected in nested expr" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); - var env = Environment.init(alloc, .standard); + var env = Environment.init(alloc); defer env.deinit(); const info = try evalStringInfo(&env, alloc, "sqrt(0b100) + 1"); try testing.expect(info.has_nondecimal_literal); @@ -1202,7 +1205,7 @@ test "exact: overflow from an absurd exponent is reported as overflow" { /// Evaluate and keep the exact result. The arena owns everything. fn testEvalNumber(arena: *std.heap.ArenaAllocator, source: []const u8) !Number { const a = arena.allocator(); - var env = Environment.init(a, .standard); + var env = Environment.init(a); defer env.deinit(); return evalString(&env, a, source); } @@ -1288,7 +1291,7 @@ test "Number API: variables keep the exactness of their expression" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const a = arena.allocator(); - var env = Environment.init(a, .standard); + var env = Environment.init(a); defer env.deinit(); const assigned = try evalString(&env, a, "X = 0.1"); @@ -1305,7 +1308,7 @@ test "Number API: Ans keeps exactness between evaluations" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const a = arena.allocator(); - var env = Environment.init(a, .standard); + var env = Environment.init(a); defer env.deinit(); _ = try evalString(&env, a, "1/3"); @@ -1318,7 +1321,7 @@ test "Number API: Ans keeps exactness between evaluations" { test "Number API: reassigning a variable releases the old value" { // Exercises the replace path in setVar, which must deinit the previous // Number rather than leaking it. - var env = Environment.init(testing.allocator, .standard); + var env = Environment.init(testing.allocator); defer env.deinit(); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); @@ -1335,7 +1338,7 @@ test "Number API: reassigning a variable releases the old value" { test "Number API: a variable name outliving its source text stays valid" { // setVar duplicates the name because it points into the expression source, // which the caller may free (the TUI frees history on Ctrl-L). - var env = Environment.init(testing.allocator, .standard); + var env = Environment.init(testing.allocator); defer env.deinit(); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); @@ -1463,7 +1466,7 @@ test "financial: results are inexact, so they do not claim exactness" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const alloc = arena.allocator(); - var env = Environment.init(alloc, .standard); + var env = Environment.init(alloc); defer env.deinit(); const result = try evalString(&env, alloc, "cagr(1000, 2000, 10)"); @@ -1501,7 +1504,7 @@ test "financial: wrong argument counts are unknown functions, not silent default // whole parsed tree on every call, which an arena silently absorbs. test "no leak: a successful evaluation releases the parsed tree" { - var env = Environment.init(testing.allocator, .standard); + var env = Environment.init(testing.allocator); defer env.deinit(); const sources = [_][]const u8{ @@ -1522,7 +1525,7 @@ test "no leak: a successful evaluation releases the parsed tree" { } test "no leak: a failed evaluation releases the parsed tree" { - var env = Environment.init(testing.allocator, .standard); + var env = Environment.init(testing.allocator); defer env.deinit(); const sources = [_][]const u8{ @@ -1548,7 +1551,7 @@ test "no leak: a failed evaluation releases the parsed tree" { test "no leak: repeated evaluation does not accumulate" { // A long interactive session is the case that made this visible: the TUI // evaluates on every Enter and never frees anything itself. - var env = Environment.init(testing.allocator, .standard); + var env = Environment.init(testing.allocator); defer env.deinit(); var i: usize = 0; @@ -1600,7 +1603,7 @@ test "a malformed group is an error, not a silently merged number" { test "a grouped literal past 2^53 is still exact" { // The separators have to reach the exact re-parse, not just the f64 channel. - var env = Environment.init(testing.allocator, .standard); + var env = Environment.init(testing.allocator); defer env.deinit(); var value = try evalString(&env, testing.allocator, "9,007,199,254,740,993"); defer value.deinit(); diff --git a/engine/src/financial.zig b/engine/src/financial.zig index cc67eca..521e37a 100644 --- a/engine/src/financial.zig +++ b/engine/src/financial.zig @@ -27,8 +27,7 @@ const std = @import("std"); const math = std.math; -const types = @import("types.zig"); -const CalcError = types.CalcError; +const CalcError = @import("errors.zig").CalcError; /// Iteration cap for the rate solver. pub const max_iterations: usize = 1000; diff --git a/engine/src/formatter.zig b/engine/src/formatter.zig index 0f871c4..58bdc92 100644 --- a/engine/src/formatter.zig +++ b/engine/src/formatter.zig @@ -17,9 +17,9 @@ //! magnitude is below the fractional budget const std = @import("std"); -const types = @import("types.zig"); -const BitWidth = types.BitWidth; -const Endianness = types.Endianness; +const Integer = @import("Integer.zig"); +const BitWidth = Integer.BitWidth; +const Endianness = std.builtin.Endian; const Number = @import("number.zig").Number; /// A formatted value with both display and clipboard representations. @@ -321,6 +321,30 @@ pub fn writeGroupedDecimal(dest: []u8, text: []const u8) usize { return w + parts.tail.len; } +/// The width to print the hex, octal and binary rows at, for a standard-mode value +/// that has no configured width (FR-1.9). +/// +/// The narrowest of the standard widths that holds `value`, so a small number does +/// not come out padded to 64 bits: 1 prints as `01`, and 256 steps up to `01 00`. +/// It rounds up to a width a reader recognises rather than to a bit count, which is +/// what makes the hex row read as whole bytes. +/// +/// Unsigned only: it counts significant bits of the pattern, so a negative value's +/// two's complement form would always report the full width. Callers reach this +/// only for non-negative integers, which is also what FR-1.9 promises. Zero has no +/// significant bits and prints at the narrowest width. +/// +/// Programmer mode does not use this; there the width is the user's setting. +pub fn displayWidthFor(value: u128) BitWidth { + return switch (128 - @clz(value)) { + 0...8 => .bits8, + 9...16 => .bits16, + 17...32 => .bits32, + 33...64 => .bits64, + else => .bits128, + }; +} + /// Format an integer for programmer mode hex display. /// Display: "FF FF FF FF" (space per byte), byte order per `endian`. /// Raw: "0xFFFFFFFF" (no separators, canonical MSB-first value regardless of @@ -624,6 +648,31 @@ test "formatFloat: zero" { try testing.expectEqualStrings("0", result.raw); } +test "displayWidthFor: the narrowest standard width that holds the value" { + const BW = Integer.BitWidth; + try testing.expectEqual(BW.bits8, displayWidthFor(0)); + try testing.expectEqual(BW.bits8, displayWidthFor(255)); + try testing.expectEqual(BW.bits16, displayWidthFor(256)); + try testing.expectEqual(BW.bits16, displayWidthFor(65535)); + try testing.expectEqual(BW.bits32, displayWidthFor(65536)); + try testing.expectEqual(BW.bits64, displayWidthFor(0x1_0000_0000)); + try testing.expectEqual(BW.bits128, displayWidthFor(0x1_0000_0000_0000_0000)); + try testing.expectEqual(BW.bits128, displayWidthFor(std.math.maxInt(u128))); +} + +test "displayWidthFor: the chosen width holds the value and sizes the rows" { + // What the width is for: the hex row of a small number is one byte, not eight. + var buf: [256]u8 = undefined; + for ([_]u128{ 0, 1, 255, 256, 0xFFFF, 0x1_0000, std.math.maxInt(u64), std.math.maxInt(u128) }) |value| { + const bw = displayWidthFor(value); + try testing.expectEqual(value, value & bw.mask()); + const hex = formatHex(&buf, value, bw, .big); + // Two hex digits per byte, plus one space between bytes. + const bytes = bw.bits() / 8; + try testing.expectEqual(@as(usize, bytes * 2 + bytes - 1), hex.display.len); + } +} + test "formatHex: 8-bit" { var buf: [256]u8 = undefined; const result = formatHex(&buf, 0xFF, .bits8, .big); @@ -1476,10 +1525,10 @@ test "splitDecimalText: one place that takes decimal text apart" { try testing.expectEqual(@as(usize, 4), negative.int_digits); try testing.expectEqualStrings(".56", negative.tail); - const integer = splitDecimalText("-70"); - try testing.expectEqual(@as(usize, 1), integer.sign_len); - try testing.expectEqual(@as(usize, 2), integer.int_digits); - try testing.expectEqualStrings("", integer.tail); + const whole = splitDecimalText("-70"); + try testing.expectEqual(@as(usize, 1), whole.sign_len); + try testing.expectEqual(@as(usize, 2), whole.int_digits); + try testing.expectEqualStrings("", whole.tail); const explicit_plus = splitDecimalText("+5"); try testing.expectEqual(@as(usize, 1), explicit_plus.sign_len); diff --git a/engine/src/number.zig b/engine/src/number.zig index 6f77722..5b0e764 100644 --- a/engine/src/number.zig +++ b/engine/src/number.zig @@ -21,7 +21,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const rational = @import("rational.zig"); const Rational = rational.Rational; -const CalcError = @import("types.zig").CalcError; +const CalcError = @import("errors.zig").CalcError; /// Map a numeric-model error onto the engine's error set. /// @@ -624,7 +624,7 @@ test "sqrt: the domain error reaches the engine error set as a domain error" { } test "toCalcError maps every numeric error, with no default" { - const CalcErr = @import("types.zig").CalcError; + 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. diff --git a/engine/src/parser.zig b/engine/src/parser.zig index df79ae4..7f8bc45 100644 --- a/engine/src/parser.zig +++ b/engine/src/parser.zig @@ -20,8 +20,7 @@ const Tokenizer = tokenizer_mod.Tokenizer; const TokenKind = tokenizer_mod.TokenKind; const Token = tokenizer_mod.Token; const parseNumber = tokenizer_mod.parseNumber; -const types = @import("types.zig"); -const CalcError = types.CalcError; +const CalcError = @import("errors.zig").CalcError; /// Precedence levels (higher = tighter binding). /// diff --git a/engine/src/programmer.zig b/engine/src/programmer.zig index 6963e67..43b9375 100644 --- a/engine/src/programmer.zig +++ b/engine/src/programmer.zig @@ -1,38 +1,47 @@ //! Programmer mode evaluator for Tally. //! -//! All operations use exact integer arithmetic (u128 storage), with results -//! masked to the configured bit width. No floating-point involved. -//! Produces Integer values with signed/unsigned interpretation. +//! All operations use exact integer arithmetic (u128 storage), with results masked +//! to the configured width. No floating-point involved. The bitwise operators, the +//! shifts and the rotations live in `bitwise.zig`, which standard mode shares; what +//! is here is the wrapping arithmetic and the walk over the tree. const std = @import("std"); const Allocator = std.mem.Allocator; const ast = @import("ast.zig"); const Expr = ast.Expr; const BinaryOp = ast.BinaryOp; -const types = @import("types.zig"); -const Integer = types.Integer; -const ProgrammerConfig = types.ProgrammerConfig; -const CalcError = types.CalcError; +const Integer = @import("Integer.zig"); +const IntType = Integer.IntType; +const Endianness = std.builtin.Endian; +const CalcError = @import("errors.zig").CalcError; const parser_mod = @import("parser.zig"); const Parser = parser_mod.Parser; const bitwise = @import("bitwise.zig"); +/// What programmer mode needs to know: the integer type to compute in, and one +/// display preference that does not affect arithmetic. +pub const Config = struct { + int_type: IntType = .{}, + /// Byte order for the HEX and ASCII rows only. Defaults to big-endian so the + /// HEX row reads as the number itself (matching DEC/OCT/BIN); the little-endian + /// view (x86 memory layout) is available via the toggle. + display_endian: Endianness = .big, +}; + /// Evaluate an AST in programmer mode, producing an exact integer result. -pub fn evalProgrammer(config: ProgrammerConfig, expr: *const Expr) CalcError!Integer { - const raw = try evalExpr(config, expr); +pub fn evalProgrammer(config: Config, expr: *const Expr) CalcError!Integer { return .{ - .raw = raw & config.bit_width.mask(), - .bit_width = config.bit_width, - .signedness = config.signedness, + .raw = try evalExpr(config, expr), + .int_type = config.int_type, }; } /// Recursively evaluate an expression to a raw u128. -fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u128 { +fn evalExpr(config: Config, expr: *const Expr) CalcError!u128 { switch (expr.*) { .number => |n| { if (n.int_value) |int_val| { - return int_val & config.bit_width.mask(); + return int_val & config.int_type.mask(); } // Float literal in programmer mode: truncate to integer. // (Number literals are always non-negative; unary minus is a @@ -45,19 +54,19 @@ fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u128 { if (!std.math.isFinite(value) or value < 0) return CalcError.DomainError; if (value >= 340282366920938463463374607431768211456.0) return CalcError.Overflow; const val: u128 = @intFromFloat(value); - return val & config.bit_width.mask(); + return val & config.int_type.mask(); }, .string_literal => |text| { // Pack ASCII bytes into integer. // Big-endian packing: first char -> most significant used byte. - const max_bytes = @as(usize, config.bit_width.bits()) / 8; + const max_bytes = @as(usize, config.int_type.bits()) / 8; if (text.len > max_bytes) return CalcError.Overflow; var result: u128 = 0; for (text) |byte| { if (byte > 0x7F) return CalcError.InvalidNumber; result = (result << 8) | byte; } - return result & config.bit_width.mask(); + return result & config.int_type.mask(); }, .variable => { return CalcError.UnknownVariable; @@ -67,7 +76,7 @@ fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u128 { }, .unary => |u| { const operand = try evalExpr(config, u.operand); - const domain = bitwise.Domain.fromConfig(config); + const domain = config.int_type; return switch (u.op) { .negate => bitwise.negate(domain, operand), .bitwise_not => bitwise.not(domain, operand), @@ -91,8 +100,8 @@ fn evalExpr(config: ProgrammerConfig, 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: ProgrammerConfig, op: BinaryOp, left: u128, right: u128) CalcError!u128 { - const mask = config.bit_width.mask(); +fn evalBinaryOp(config: Config, op: BinaryOp, left: u128, right: u128) CalcError!u128 { + const mask = config.int_type.mask(); const result: u128 = switch (op) { .add => (left +% right) & mask, @@ -121,7 +130,7 @@ fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u128, right: u128) // resolves the operator at comptime, so an operator added to `BinaryOp` // that `bitwise.fromBinaryOp` does not know is a compile error here. inline else => |fixed_op| try bitwise.apply( - bitwise.Domain.fromConfig(config), + config.int_type, comptime bitwise.fromBinaryOp(fixed_op).?, left, right, @@ -132,7 +141,7 @@ fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u128, right: u128) } /// High-level: parse and evaluate a string in programmer mode. -pub fn evalProgrammerString(allocator: Allocator, source: []const u8, config: ProgrammerConfig) CalcError!Integer { +pub fn evalProgrammerString(allocator: Allocator, source: []const u8, config: Config) CalcError!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 @@ -149,10 +158,12 @@ fn testProg(source: []const u8) !Integer { return testProgWith(source, .{}); } -fn testProgWith(source: []const u8, config: ProgrammerConfig) !Integer { +/// Tests care about the integer type, never about the display byte order, so they +/// pass the type directly. +fn testProgWith(source: []const u8, int_type: IntType) !Integer { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); - return evalProgrammerString(arena.allocator(), source, config); + return evalProgrammerString(arena.allocator(), source, .{ .int_type = int_type }); } test "prog: simple number" { @@ -177,7 +188,7 @@ test "prog: addition" { test "prog: subtraction wrapping" { // 5 - 10 in 8-bit unsigned wraps - const result = try testProgWith("5 - 10", .{ .bit_width = .bits8 }); + const result = try testProgWith("5 - 10", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 251), result.unsignedValue()); // 256 - 5 try testing.expectEqual(@as(i128, -5), result.signedValue()); } @@ -188,7 +199,7 @@ test "prog: multiplication" { } test "prog: multiplication overflow 8-bit" { - const result = try testProgWith("200 * 2", .{ .bit_width = .bits8 }); + const result = try testProgWith("200 * 2", .{ .width = .bits8 }); // 400 & 0xFF = 144 try testing.expectEqual(@as(u128, 144), result.unsignedValue()); } @@ -239,22 +250,22 @@ test "prog: and/or/not keywords" { try testing.expectEqual(@as(u128, 0x0F), a.unsignedValue()); const o = try testProg("0xF0 or 0x0F"); try testing.expectEqual(@as(u128, 0xFF), o.unsignedValue()); - const n = try testProgWith("not 0x0F", .{ .bit_width = .bits8 }); + const n = try testProgWith("not 0x0F", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0xF0), n.unsignedValue()); } test "prog: bitwise NOT 8-bit" { - const result = try testProgWith("~0x0F", .{ .bit_width = .bits8 }); + const result = try testProgWith("~0x0F", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0xF0), result.unsignedValue()); } test "prog: bitwise NOT 16-bit" { - const result = try testProgWith("~0x00FF", .{ .bit_width = .bits16 }); + const result = try testProgWith("~0x00FF", .{ .width = .bits16 }); try testing.expectEqual(@as(u128, 0xFF00), result.unsignedValue()); } test "prog: bitwise NOT 32-bit" { - const result = try testProgWith("~0", .{ .bit_width = .bits32 }); + const result = try testProgWith("~0", .{ .width = .bits32 }); try testing.expectEqual(@as(u128, 0xFFFF_FFFF), result.unsignedValue()); } @@ -265,52 +276,52 @@ test "prog: shift left" { test "prog: shift left past the width shifts everything out" { // The distance used to be clamped to width - 1, so this gave 128. - const result = try testProgWith("1 << 8", .{ .bit_width = .bits8 }); + const result = try testProgWith("1 << 8", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0), result.unsignedValue()); // One less than the width still keeps the bit. - const edge = try testProgWith("1 << 7", .{ .bit_width = .bits8 }); + const edge = try testProgWith("1 << 7", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 128), edge.unsignedValue()); } test "prog: logical shift right" { - const result = try testProgWith("0x80 >>> 4", .{ .bit_width = .bits8 }); + const result = try testProgWith("0x80 >>> 4", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0x08), result.unsignedValue()); } test "prog: arithmetic shift right (sign bit preserved)" { // 0x80 in 8-bit is -128; >> 1 should give 0xC0 (-64) - const result = try testProgWith("0x80 >> 1", .{ .bit_width = .bits8 }); + const result = try testProgWith("0x80 >> 1", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0xC0), result.unsignedValue()); try testing.expectEqual(@as(i128, -64), result.signedValue()); } test "prog: arithmetic shift right (positive)" { // 0x40 in 8-bit is positive; >> 1 should give 0x20 - const result = try testProgWith("0x40 >> 1", .{ .bit_width = .bits8 }); + const result = try testProgWith("0x40 >> 1", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0x20), result.unsignedValue()); } test "prog: rotate left 8-bit" { // 0x81 rol 1 in 8-bit: bit 7 wraps to bit 0 -> 0x03 - const result = try testProgWith("0x81 rol 1", .{ .bit_width = .bits8 }); + const result = try testProgWith("0x81 rol 1", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0x03), result.unsignedValue()); } test "prog: rotate right 8-bit" { // 0x81 ror 1 in 8-bit: bit 0 wraps to bit 7 -> 0xC0 - const result = try testProgWith("0x81 ror 1", .{ .bit_width = .bits8 }); + const result = try testProgWith("0x81 ror 1", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0xC0), result.unsignedValue()); } test "prog: negation two's complement" { - const result = try testProgWith("-1", .{ .bit_width = .bits8 }); + const result = try testProgWith("-1", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue()); try testing.expectEqual(@as(i128, -1), result.signedValue()); } test "prog: negation 16-bit" { - const result = try testProgWith("-42", .{ .bit_width = .bits16 }); + const result = try testProgWith("-42", .{ .width = .bits16 }); try testing.expectEqual(@as(i128, -42), result.signedValue()); } @@ -334,23 +345,23 @@ test "prog: chained shifts" { test "prog: mask applied to input" { // 0x1FF in 8-bit mode should be masked to 0xFF - const result = try testProgWith("0x1FF", .{ .bit_width = .bits8 }); + const result = try testProgWith("0x1FF", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue()); } test "prog: 32-bit operations" { - const result = try testProgWith("0xFFFF_FFFF + 1", .{ .bit_width = .bits32 }); + const result = try testProgWith("0xFFFF_FFFF + 1", .{ .width = .bits32 }); try testing.expectEqual(@as(u128, 0), result.unsignedValue()); } test "prog: 64-bit max" { - const result = try testProgWith("~0", .{ .bit_width = .bits64 }); + const result = try testProgWith("~0", .{ .width = .bits64 }); try testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF), result.unsignedValue()); } test "prog: negative float input" { // -5.0 as a float in programmer mode should become two's complement - const result = try testProgWith("-5", .{ .bit_width = .bits8 }); + const result = try testProgWith("-5", .{ .width = .bits8 }); try testing.expectEqual(@as(i128, -5), result.signedValue()); } @@ -399,7 +410,7 @@ test "prog: ASCII literal in expression" { 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'", .{ .bit_width = .bits8 }); + const result = evalProgrammerString(arena.allocator(), "'AB'", .{ .int_type = .{ .width = .bits8 } }); try testing.expectError(CalcError.Overflow, result); } @@ -413,30 +424,30 @@ test "prog: arithmetic shift right past the width leaves the sign fill" { // 0xFF in 8-bit is -1; shifting a negative value all the way out leaves every // bit set, which is still -1. The distance used to be clamped to width - 1, // which reached the same answer here for the wrong reason. - const result = try testProgWith("0xFF >> 20", .{ .bit_width = .bits8 }); + const result = try testProgWith("0xFF >> 20", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue()); try testing.expectEqual(@as(i128, -1), result.signedValue()); } test "prog: logical shift right past the width shifts everything out" { // Used to clamp the distance to 7 and give 1. - const result = try testProgWith("0xFF >>> 20", .{ .bit_width = .bits8 }); + const result = try testProgWith("0xFF >>> 20", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0), result.unsignedValue()); // One less than the width still keeps the bottom bit. - const edge = try testProgWith("0xFF >>> 7", .{ .bit_width = .bits8 }); + const edge = try testProgWith("0xFF >>> 7", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 1), edge.unsignedValue()); } test "prog: arithmetic shift right past the width, positive value" { // 0x40 in 8-bit is positive, so the fill is zeros and everything shifts out. - const result = try testProgWith("0x40 >> 20", .{ .bit_width = .bits8 }); + const result = try testProgWith("0x40 >> 20", .{ .width = .bits8 }); try testing.expectEqual(@as(u128, 0), result.unsignedValue()); } test "no leak: evalProgrammerString releases the parsed tree" { // testing.allocator rather than an arena, so a retained AST fails the test. - const config: ProgrammerConfig = .{}; + const config: Config = .{}; const good = [_][]const u8{ "0xFF and 0x0F", "1 << 8", "not 0", "0b1010 xor 0b0101", "5 rol 2" }; for (good) |source| { _ = try evalProgrammerString(std.testing.allocator, source, config); @@ -450,7 +461,7 @@ test "no leak: evalProgrammerString releases the parsed tree" { } test "programmer mode: a float literal out of range errors instead of aborting" { - const config: ProgrammerConfig = .{}; + const config: Config = .{}; // `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( @@ -469,7 +480,7 @@ test "programmer mode: a float literal out of range errors instead of aborting" } test "programmer mode: an infinite or NaN literal is a domain error" { - const config: ProgrammerConfig = .{}; + const config: Config = .{}; // 10^400 overflows the exact tier's float projection to infinity. try std.testing.expectError( CalcError.DomainError, diff --git a/engine/src/tokenizer.zig b/engine/src/tokenizer.zig index a0fcabe..7264e8b 100644 --- a/engine/src/tokenizer.zig +++ b/engine/src/tokenizer.zig @@ -6,8 +6,16 @@ //! literals, and space/comma/underscore digit separators. const std = @import("std"); -const types = @import("types.zig"); -const Base = types.Base; + +/// The base a numeric literal was written in. A lexical property: the tokenizer +/// determines it from the prefix, and the AST carries it so the evaluator can tell +/// `0x10` from `16` for the multi-base display (FR-1.9). +pub const Base = enum { + decimal, + hex, + octal, + binary, +}; pub const TokenKind = enum { // Literals diff --git a/engine/src/types.zig b/engine/src/types.zig deleted file mode 100644 index 7643732..0000000 --- a/engine/src/types.zig +++ /dev/null @@ -1,274 +0,0 @@ -//! Core types shared across the Tally engine. - -const std = @import("std"); - -/// Calculation mode determines parsing and evaluation behavior. -pub const Mode = enum { - standard, - programmer, - financial, -}; - -/// Configurable integer bit width for programmer mode. -pub const BitWidth = enum(u8) { - bits8 = 8, - bits16 = 16, - bits32 = 32, - bits64 = 64, - bits128 = 128, - - /// Returns the mask for this bit width (all bits set within width). - pub fn mask(self: BitWidth) u128 { - return switch (self) { - .bits8 => 0xFF, - .bits16 => 0xFFFF, - .bits32 => 0xFFFF_FFFF, - .bits64 => 0xFFFF_FFFF_FFFF_FFFF, - .bits128 => 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF, - }; - } - - /// Returns the number of bits as a plain integer. - pub fn bits(self: BitWidth) u8 { - return @intFromEnum(self); - } - - /// Returns the smallest standard width that can hold the given value. - pub fn smallestFor(value: u128) BitWidth { - if (value <= 0xFF) return .bits8; - if (value <= 0xFFFF) return .bits16; - if (value <= 0xFFFF_FFFF) return .bits32; - if (value <= 0xFFFF_FFFF_FFFF_FFFF) return .bits64; - return .bits128; - } -}; - -/// Signedness interpretation for programmer mode values. -pub const Signedness = enum { - signed, - unsigned, -}; - -/// Byte ordering for display purposes. -pub const Endianness = enum { - little, - big, -}; - -/// Number base for input/output. -pub const Base = enum { - decimal, - hex, - octal, - binary, -}; - -/// An integer value in programmer mode. -/// Raw bits stored in u128; interpretation depends on bit_width and signedness. -pub const Integer = struct { - raw: u128, - bit_width: BitWidth, - signedness: Signedness, - - /// Apply the bit width mask, truncating to the configured width. - pub fn masked(self: Integer) u128 { - return self.raw & self.bit_width.mask(); - } - - /// Interpret as a signed value (sign-extend from bit_width). - pub fn signedValue(self: Integer) i128 { - const m = self.masked(); - const width = self.bit_width.bits(); - const sign_bit: u128 = @as(u128, 1) << @intCast(width - 1); - if (m & sign_bit != 0) { - // Sign extend: fill upper bits with 1s - const extension = ~self.bit_width.mask(); - return @bitCast(m | extension); - } - return @intCast(m); - } - - /// Interpret as an unsigned value (just mask). - pub fn unsignedValue(self: Integer) u128 { - return self.masked(); - } -}; - -/// Programmer mode configuration. -pub const ProgrammerConfig = struct { - bit_width: BitWidth = .bits64, - signedness: Signedness = .signed, - /// Byte order for the HEX and ASCII rows only. Defaults to big-endian so - /// the HEX row reads as the number itself (matching DEC/OCT/BIN); the - /// little-endian view (x86 memory layout) is available via the toggle. - display_endian: Endianness = .big, -}; - -/// 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. -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", - }; -} - -test "BitWidth.mask" { - try std.testing.expectEqual(@as(u128, 0xFF), BitWidth.bits8.mask()); - try std.testing.expectEqual(@as(u128, 0xFFFF), BitWidth.bits16.mask()); - try std.testing.expectEqual(@as(u128, 0xFFFF_FFFF), BitWidth.bits32.mask()); - try std.testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF), BitWidth.bits64.mask()); - try std.testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF), BitWidth.bits128.mask()); -} - -test "Integer.signedValue" { - // 0xFF in 8-bit signed = -1 - const i8_neg1 = Integer{ .raw = 0xFF, .bit_width = .bits8, .signedness = .signed }; - try std.testing.expectEqual(@as(i128, -1), i8_neg1.signedValue()); - - // 0x7F in 8-bit signed = 127 - const i8_127 = Integer{ .raw = 0x7F, .bit_width = .bits8, .signedness = .signed }; - try std.testing.expectEqual(@as(i128, 127), i8_127.signedValue()); - - // 0x80 in 8-bit signed = -128 - const i8_neg128 = Integer{ .raw = 0x80, .bit_width = .bits8, .signedness = .signed }; - try std.testing.expectEqual(@as(i128, -128), i8_neg128.signedValue()); - - // 0xFFFF_FFFF in 32-bit signed = -1 - const i32_neg1 = Integer{ .raw = 0xFFFF_FFFF, .bit_width = .bits32, .signedness = .signed }; - try std.testing.expectEqual(@as(i128, -1), i32_neg1.signedValue()); -} - -test "Integer.unsignedValue masks correctly" { - // Extra bits beyond width are masked off - const val = Integer{ .raw = 0x1FF, .bit_width = .bits8, .signedness = .unsigned }; - try std.testing.expectEqual(@as(u128, 0xFF), val.unsignedValue()); -} - -test "BitWidth.smallestFor" { - try std.testing.expectEqual(BitWidth.bits8, BitWidth.smallestFor(0)); - try std.testing.expectEqual(BitWidth.bits8, BitWidth.smallestFor(255)); - try std.testing.expectEqual(BitWidth.bits16, BitWidth.smallestFor(256)); - try std.testing.expectEqual(BitWidth.bits16, BitWidth.smallestFor(65535)); - try std.testing.expectEqual(BitWidth.bits32, BitWidth.smallestFor(65536)); - try std.testing.expectEqual(BitWidth.bits64, BitWidth.smallestFor(0x1_0000_0000)); - try std.testing.expectEqual(BitWidth.bits128, BitWidth.smallestFor(0x1_0000_0000_0000_0000)); -} - -// -- One error phrase table -- -// -// The CLI and the TUI each had a full switch over this error set, and the TUI's had -// already fallen behind: InsufficientParameters, ConvergenceFailure and -// InvalidExpression all came out as "evaluation error". The phrases now live here -// once and each frontend adds its own decoration at comptime. - -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 std.testing.expect(phrase.len > 0); - // No prefix and no newline: decoration belongs to the caller. - try std.testing.expect(!std.mem.startsWith(u8, phrase, "error")); - try std.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 std.testing.expectEqualStrings("error: division by zero", decorated); -} - -test "errorPhrase: the cases the TUI table used to lose" { - try std.testing.expectEqualStrings("invalid expression", errorPhrase(CalcError.InvalidExpression)); - try std.testing.expectEqualStrings("no solution found", errorPhrase(CalcError.ConvergenceFailure)); - try std.testing.expectEqualStrings( - "these values do not determine an answer", - errorPhrase(CalcError.InsufficientParameters), - ); -} diff --git a/engine/src/units.zig b/engine/src/units.zig index 47473f6..4bcdd6d 100644 --- a/engine/src/units.zig +++ b/engine/src/units.zig @@ -19,8 +19,7 @@ //! No allocation, no I/O. Adding a unit means adding a table entry. const std = @import("std"); -const types = @import("types.zig"); -const CalcError = types.CalcError; +const CalcError = @import("errors.zig").CalcError; const rational_mod = @import("rational.zig"); const Rational = rational_mod.Rational; const number_mod = @import("number.zig"); diff --git a/src/main.zig b/src/main.zig index 92bb94c..2893230 100644 --- a/src/main.zig +++ b/src/main.zig @@ -8,16 +8,23 @@ pub const CliResult = struct { is_error: bool, }; +/// Which evaluator a CLI invocation wants. +/// +/// The CLI's own concept, not the engine's: the engine has `evalString` and +/// `evalProgrammerString` and no notion of a mode. There are exactly two here +/// because `-p` is the only mode flag; the TUI's four tabs are its own enum. +pub const Mode = enum { standard, programmer }; + /// Parse CLI args and determine the expression and mode. /// Returns the joined expression and mode, or an error/help output. pub const ParsedArgs = union(enum) { expression: struct { text: []const u8, - mode: engine.Mode, + mode: Mode, /// Width, signedness and byte order for programmer mode. The CLI accepts /// the same settings the TUI has, so a session in one can be reproduced in /// the other. - config: engine.types.ProgrammerConfig = .{}, + config: engine.programmer.Config = .{}, }, conversion: struct { /// Kept as text so it can be parsed exactly rather than through f64. @@ -37,8 +44,8 @@ pub const ParsedArgs = union(enum) { }; pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedArgs { - var mode: engine.Mode = .standard; - var config: engine.types.ProgrammerConfig = .{}; + var mode: Mode = .standard; + var config: engine.programmer.Config = .{}; var expr_parts = std.ArrayList([]const u8).empty; defer expr_parts.deinit(allocator); @@ -67,10 +74,10 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedA } else if (std.mem.eql(u8, arg, "--version")) { return .{ .output = .{ .text = "tally 0.1.0\n", .is_error = false } }; } else if (std.mem.eql(u8, arg, "--signed")) { - config.signedness = .signed; + config.int_type.signedness = .signed; mode = .programmer; } else if (std.mem.eql(u8, arg, "--unsigned")) { - config.signedness = .unsigned; + config.int_type.signedness = .unsigned; mode = .programmer; } else { // Flags that take a value, accepted as either `--bits 8` or `--bits=8`. @@ -78,7 +85,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedA .absent => {}, .missing => return .{ .output = .{ .text = bits_usage, .is_error = true } }, .value => |text| { - config.bit_width = parseBitWidth(text) orelse + config.int_type.width = parseBitWidth(text) orelse return .{ .output = .{ .text = bits_usage, .is_error = true } }; // The width only means something in programmer mode: standard // mode is fixed at 64-bit signed. Asking for a width is asking @@ -147,10 +154,10 @@ fn flagValue(arg: []const u8, name: []const u8, args: []const []const u8, index: return .absent; } -fn parseBitWidth(text: []const u8) ?engine.types.BitWidth { +fn parseBitWidth(text: []const u8) ?engine.Integer.BitWidth { // Driven by the enum, so a width added to `BitWidth` is accepted here without // a second list to update. - inline for (@typeInfo(engine.types.BitWidth).@"enum".fields) |field| { + inline for (@typeInfo(engine.Integer.BitWidth).@"enum".fields) |field| { if (std.mem.eql(u8, text, comptime std.fmt.comptimePrint("{d}", .{field.value}))) { return @enumFromInt(field.value); } @@ -161,7 +168,7 @@ fn parseBitWidth(text: []const u8) ?engine.types.BitWidth { /// The accepted widths, as they appear in messages: "8, 16, 32, 64, 128". const bit_width_list = blk: { var list: []const u8 = ""; - for (@typeInfo(engine.types.BitWidth).@"enum".fields, 0..) |field, i| { + for (@typeInfo(engine.Integer.BitWidth).@"enum".fields, 0..) |field, i| { list = list ++ (if (i == 0) "" else ", ") ++ std.fmt.comptimePrint("{d}", .{field.value}); } break :blk list; @@ -176,7 +183,7 @@ comptime { } } -fn parseEndian(text: []const u8) ?engine.types.Endianness { +fn parseEndian(text: []const u8) ?std.builtin.Endian { if (std.ascii.eqlIgnoreCase(text, "big") or std.ascii.eqlIgnoreCase(text, "be")) return .big; if (std.ascii.eqlIgnoreCase(text, "little") or std.ascii.eqlIgnoreCase(text, "le")) return .little; return null; @@ -404,7 +411,7 @@ fn formatConversionUnits( } /// Evaluate an expression and format the result as a string. -pub fn evaluate(allocator: std.mem.Allocator, expression: []const u8, mode: engine.Mode, buf: []u8) CliResult { +pub fn evaluate(allocator: std.mem.Allocator, expression: []const u8, mode: Mode, buf: []u8) CliResult { return evaluateWith(allocator, expression, mode, .{}, buf); } @@ -413,8 +420,8 @@ pub fn evaluate(allocator: std.mem.Allocator, expression: []const u8, mode: engi pub fn evaluateWith( allocator: std.mem.Allocator, expression: []const u8, - mode: engine.Mode, - config: engine.types.ProgrammerConfig, + mode: Mode, + config: engine.programmer.Config, buf: []u8, ) CliResult { if (mode == .programmer) { @@ -424,7 +431,7 @@ pub fn evaluateWith( return formatProgrammerResult(buf, result, config); } - var env = engine.Environment.init(allocator, .standard); + var env = engine.Environment.init(allocator); defer env.deinit(); // A standalone "to" keyword means this is a unit conversion, e.g. @@ -475,7 +482,7 @@ fn isDisplayableInt(value: f64) bool { /// The decimal display is already computed; append hex/oct/bin. fn formatStandardMultiBase(buf: []u8, dec_display: []const u8, value: f64) CliResult { const int_val: u128 = @intFromFloat(value); - const bw = engine.types.BitWidth.smallestFor(int_val); + const bw = engine.formatter.displayWidthFor(int_val); var hex_buf: [256]u8 = undefined; var oct_buf: [256]u8 = undefined; @@ -501,7 +508,7 @@ fn formatStandardMultiBase(buf: []u8, dec_display: []const u8, value: f64) CliRe return .{ .output = output, .is_error = false }; } -fn formatProgrammerResult(buf: []u8, result: engine.types.Integer, config: engine.types.ProgrammerConfig) CliResult { +fn formatProgrammerResult(buf: []u8, result: engine.Integer, config: engine.programmer.Config) CliResult { const value = result.unsignedValue(); const signed = result.signedValue(); @@ -511,11 +518,11 @@ fn formatProgrammerResult(buf: []u8, result: engine.types.Integer, config: engin var oct_buf: [256]u8 = undefined; var bin_buf: [512]u8 = undefined; - const hex = engine.formatter.formatHex(&hex_buf, value, config.bit_width, config.display_endian); + const hex = engine.formatter.formatHex(&hex_buf, value, config.int_type.width, config.display_endian); const dec = engine.formatter.formatDecimalUnsigned(&dec_buf, value); const sdec = engine.formatter.formatDecimalSigned(&sdec_buf, signed); - const oct = engine.formatter.formatOctal(&oct_buf, value, config.bit_width); - const bin = engine.formatter.formatBinary(&bin_buf, value, config.bit_width); + const oct = engine.formatter.formatOctal(&oct_buf, value, config.int_type.width); + const bin = engine.formatter.formatBinary(&bin_buf, value, config.int_type.width); const output = std.fmt.bufPrint(buf, \\ dec(signed): {s} @@ -533,7 +540,7 @@ fn formatProgrammerResult(buf: []u8, result: engine.types.Integer, config: engin /// Turn an engine error into a CLI line. /// -/// The phrases live once, in `engine.types.errorPhrase`. This adds the prefix and +/// The phrases live once, in `engine.errors.errorPhrase`. 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 @@ -549,7 +556,7 @@ fn errorMessage(err: engine.CalcError) []const u8 { /// silently reads "evaluation error". fn decoratedError(err: engine.CalcError) []const u8 { return switch (err) { - inline else => |e| comptime "error: " ++ engine.types.errorPhrase(e) ++ "\n", + inline else => |e| comptime "error: " ++ engine.errors.errorPhrase(e) ++ "\n", }; } @@ -888,7 +895,7 @@ test "parseArgs: simple expression" { switch (parsed) { .expression => |e| { try testing.expectEqualStrings("2+2", e.text); - try testing.expectEqual(engine.Mode.standard, e.mode); + try testing.expectEqual(Mode.standard, e.mode); testing.allocator.free(e.text); }, else => unreachable, @@ -910,7 +917,7 @@ test "parseArgs: programmer flag" { const parsed = parseArgs(testing.allocator, &.{ "-p", "0xFF" }); switch (parsed) { .expression => |e| { - try testing.expectEqual(engine.Mode.programmer, e.mode); + try testing.expectEqual(Mode.programmer, e.mode); try testing.expectEqualStrings("0xFF", e.text); testing.allocator.free(e.text); }, @@ -922,7 +929,7 @@ test "parseArgs: --programmer long flag" { const parsed = parseArgs(testing.allocator, &.{ "--programmer", "0xF0", "|", "0x0F" }); switch (parsed) { .expression => |e| { - try testing.expectEqual(engine.Mode.programmer, e.mode); + try testing.expectEqual(Mode.programmer, e.mode); try testing.expectEqualStrings("0xF0 | 0x0F", e.text); testing.allocator.free(e.text); }, @@ -937,16 +944,16 @@ test "parseArgs: --bits sets the width, in either spelling, and implies -p" { }) |args| { const e = parseArgs(testing.allocator, args).expression; defer testing.allocator.free(e.text); - try testing.expectEqual(engine.types.BitWidth.bits8, e.config.bit_width); + try testing.expectEqual(engine.Integer.BitWidth.bits8, e.config.int_type.width); // Asking for a width is asking for programmer mode: standard mode is fixed // at 64-bit signed, so the flag would mean nothing there. - try testing.expectEqual(engine.Mode.programmer, e.mode); + try testing.expectEqual(Mode.programmer, e.mode); try testing.expectEqualStrings("0xFF", e.text); } } test "parseArgs: every documented bit width is accepted" { - for ([_]struct { text: []const u8, expected: engine.types.BitWidth }{ + for ([_]struct { text: []const u8, expected: engine.Integer.BitWidth }{ .{ .text = "8", .expected = .bits8 }, .{ .text = "16", .expected = .bits16 }, .{ .text = "32", .expected = .bits32 }, @@ -964,13 +971,13 @@ test "parseArgs: every documented bit width is accepted" { test "parseArgs: --signed and --unsigned set the signedness and imply -p" { const signed = parseArgs(testing.allocator, &.{ "--signed", "0xFF" }).expression; defer testing.allocator.free(signed.text); - try testing.expectEqual(engine.types.Signedness.signed, signed.config.signedness); - try testing.expectEqual(engine.Mode.programmer, signed.mode); + try testing.expectEqual(engine.Integer.Signedness.signed, signed.config.int_type.signedness); + try testing.expectEqual(Mode.programmer, signed.mode); const unsigned = parseArgs(testing.allocator, &.{ "--unsigned", "0xFF" }).expression; defer testing.allocator.free(unsigned.text); - try testing.expectEqual(engine.types.Signedness.unsigned, unsigned.config.signedness); - try testing.expectEqual(engine.Mode.programmer, unsigned.mode); + try testing.expectEqual(engine.Integer.Signedness.unsigned, unsigned.config.int_type.signedness); + try testing.expectEqual(Mode.programmer, unsigned.mode); } test "parseArgs: --endian sets the byte order and accepts both spellings" { @@ -981,10 +988,10 @@ test "parseArgs: --endian sets the byte order and accepts both spellings" { }) |args| { const e = parseArgs(testing.allocator, args).expression; defer testing.allocator.free(e.text); - try testing.expectEqual(engine.types.Endianness.little, e.config.display_endian); + try testing.expectEqual(std.builtin.Endian.little, e.config.display_endian); } - try testing.expectEqual(engine.types.Endianness.big, parseEndian("big").?); - try testing.expectEqual(engine.types.Endianness.big, parseEndian("BE").?); + try testing.expectEqual(std.builtin.Endian.big, parseEndian("big").?); + try testing.expectEqual(std.builtin.Endian.big, parseEndian("BE").?); try testing.expect(parseEndian("middle") == null); try testing.expect(parseEndian("") == null); } @@ -992,9 +999,9 @@ test "parseArgs: --endian sets the byte order and accepts both spellings" { test "parseArgs: the flags combine, and order does not matter" { const e = parseArgs(testing.allocator, &.{ "--unsigned", "--bits", "16", "--endian", "little", "0xFF", "+", "1" }).expression; defer testing.allocator.free(e.text); - try testing.expectEqual(engine.types.BitWidth.bits16, e.config.bit_width); - try testing.expectEqual(engine.types.Signedness.unsigned, e.config.signedness); - try testing.expectEqual(engine.types.Endianness.little, e.config.display_endian); + try testing.expectEqual(engine.Integer.BitWidth.bits16, e.config.int_type.width); + try testing.expectEqual(engine.Integer.Signedness.unsigned, e.config.int_type.signedness); + try testing.expectEqual(std.builtin.Endian.little, e.config.display_endian); try testing.expectEqualStrings("0xFF + 1", e.text); } @@ -1043,31 +1050,29 @@ test "the programmer config reaches the result" { const alloc = arena.allocator(); // 8-bit: 0xFF + 1 wraps to 0. - const narrow = evaluateWith(alloc, "0xFF + 1", .programmer, .{ .bit_width = .bits8 }, &buf); + const narrow = evaluateWith(alloc, "0xFF + 1", .programmer, .{ .int_type = .{ .width = .bits8 } }, &buf); try testing.expect(!narrow.is_error); try testing.expect(std.mem.indexOf(u8, narrow.output, "dec(unsigned): 0\n") != null); // Signedness decides whether >> extends the sign. const signed = evaluateWith(alloc, "0xFF >> 1", .programmer, .{ - .bit_width = .bits8, - .signedness = .signed, + .int_type = .{ .width = .bits8, .signedness = .signed }, }, &buf); try testing.expect(std.mem.indexOf(u8, signed.output, "dec(signed): -1") != null); const unsigned = evaluateWith(alloc, "0xFF >> 1", .programmer, .{ - .bit_width = .bits8, - .signedness = .unsigned, + .int_type = .{ .width = .bits8, .signedness = .unsigned }, }, &buf); try testing.expect(std.mem.indexOf(u8, unsigned.output, "dec(unsigned): 127") != null); // Byte order reverses the hex row and nothing else. const little = evaluateWith(alloc, "0xDEAD", .programmer, .{ - .bit_width = .bits16, + .int_type = .{ .width = .bits16 }, .display_endian = .little, }, &buf); try testing.expect(std.mem.indexOf(u8, little.output, "hex: AD DE") != null); const big = evaluateWith(alloc, "0xDEAD", .programmer, .{ - .bit_width = .bits16, + .int_type = .{ .width = .bits16 }, .display_endian = .big, }, &buf); try testing.expect(std.mem.indexOf(u8, big.output, "hex: DE AD") != null); diff --git a/src/tui.zig b/src/tui.zig index 8b355b9..f133eb7 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -132,7 +132,7 @@ pub const App = struct { mode: Mode, // Programmer mode state prog_value: u128, - prog_config: engine.types.ProgrammerConfig, + prog_config: engine.programmer.Config, bit_cursor: u7, prog_field: ProgField, value_zone_active: bool, // true = cursor in value display, false = in input @@ -219,7 +219,7 @@ pub const App = struct { return .{ .allocator = allocator, .io = io, - .env = engine.evaluator.Environment.init(allocator, .standard), + .env = engine.evaluator.Environment.init(allocator), .input = text_field, .history = .empty, .show_help = false, @@ -373,7 +373,7 @@ pub const App = struct { self.value_zone_active = true; self.prog_field = target.field; if (target.bit) |bit| { - if (bit < self.prog_config.bit_width.bits()) self.bit_cursor = bit; + if (bit < self.prog_config.int_type.width.bits()) self.bit_cursor = bit; } else { self.alignCursorToField(); } @@ -381,10 +381,10 @@ pub const App = struct { .toggle_bit => |bit| { self.value_zone_active = true; self.prog_field = if (self.prog_field == .bin) .bin else .bits; - if (bit < self.prog_config.bit_width.bits()) { + if (bit < self.prog_config.int_type.width.bits()) { self.bit_cursor = bit; self.prog_value ^= @as(u128, 1) << bit; - self.prog_value &= self.prog_config.bit_width.mask(); + self.prog_value &= self.prog_config.int_type.width.mask(); } }, .focus_input => self.value_zone_active = false, @@ -429,10 +429,9 @@ pub const App = struct { /// Switch modes, carrying the last answer into programmer mode. fn setMode(self: *App, new_mode: Mode) void { self.mode = new_mode; - self.env.mode = switch (new_mode) { - .programmer => .programmer, - else => .standard, - }; + // The environment has no mode to set: which evaluator runs is decided at + // the call, not by state. This used to write `self.env.mode`, a field the + // engine never read. if (new_mode == .programmer) self.loadAnsIntoProgrammer(); self.value_zone_active = false; } @@ -450,15 +449,15 @@ pub const App = struct { const signed: i128 = @intFromFloat(ans); self.prog_value = @bitCast(signed); } - self.prog_value &= self.prog_config.bit_width.mask(); + self.prog_value &= self.prog_config.int_type.width.mask(); } else { self.float_format = .f64; self.float_view_active = true; self.syncFloatWidth(); self.prog_value = @as(u64, @bitCast(ans)); self.prog_field = .bits; - if (self.bit_cursor >= self.prog_config.bit_width.bits()) { - self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1); + if (self.bit_cursor >= self.prog_config.int_type.width.bits()) { + self.bit_cursor = @intCast(self.prog_config.int_type.width.bits() - 1); } } } @@ -652,8 +651,8 @@ pub const App = struct { self.syncFloatWidth(); // Keep the bit grid focused so arrows/space edit bits directly. self.prog_field = .bits; - if (self.bit_cursor >= self.prog_config.bit_width.bits()) { - self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1); + if (self.bit_cursor >= self.prog_config.int_type.width.bits()) { + self.bit_cursor = @intCast(self.prog_config.int_type.width.bits() - 1); } } ctx.redraw = true; @@ -790,7 +789,7 @@ pub const App = struct { // Up/Down: move between fields if (key.matches(vaxis.Key.up, .{})) { if (self.prog_field == .bits) { - const width = self.prog_config.bit_width.bits(); + const width = self.prog_config.int_type.width.bits(); const bits_per_row: u8 = if (width > 32) 32 else width; if (@as(u8, self.bit_cursor) + bits_per_row < width) { self.bit_cursor += @intCast(bits_per_row); @@ -803,7 +802,7 @@ pub const App = struct { } if (key.matches(vaxis.Key.down, .{})) { if (self.prog_field == .bits) { - const width = self.prog_config.bit_width.bits(); + const width = self.prog_config.int_type.width.bits(); const bits_per_row: u8 = if (width > 32) 32 else width; if (self.bit_cursor >= bits_per_row) { self.bit_cursor -= @intCast(bits_per_row); @@ -819,7 +818,7 @@ pub const App = struct { if (key.matches(vaxis.Key.left, .{})) { const step = self.fieldBitStep(); if (step > 0) { - const width = self.prog_config.bit_width.bits(); + const width = self.prog_config.int_type.width.bits(); if (@as(u16, self.bit_cursor) + step < width) { self.bit_cursor += @intCast(step); } @@ -840,7 +839,7 @@ pub const App = struct { if (key.matches(' ', .{})) { if (self.prog_field == .bits) { self.prog_value ^= @as(u128, 1) << self.bit_cursor; - self.prog_value &= self.prog_config.bit_width.mask(); + self.prog_value &= self.prog_config.int_type.width.mask(); } return; } @@ -868,7 +867,7 @@ pub const App = struct { const shift: u7 = self.bit_cursor & 0x7C; // round down to nibble boundary const mask = ~(@as(u128, 0xF) << shift); self.prog_value = (self.prog_value & mask) | (@as(u128, n) << shift); - self.prog_value &= self.prog_config.bit_width.mask(); + self.prog_value &= self.prog_config.int_type.width.mask(); // Move cursor right (toward LSB) if (shift >= 4) self.bit_cursor -= 4; } @@ -880,7 +879,7 @@ pub const App = struct { const shift: u7 = (self.bit_cursor / 3) * 3; // round down to octal boundary const mask = ~(@as(u128, 0x7) << shift); self.prog_value = (self.prog_value & mask) | (@as(u128, digit) << shift); - self.prog_value &= self.prog_config.bit_width.mask(); + self.prog_value &= self.prog_config.int_type.width.mask(); if (shift >= 3) self.bit_cursor -= 3; } }, @@ -890,7 +889,7 @@ pub const App = struct { if (self.bit_cursor > 0) self.bit_cursor -= 1; } else if (char == '1') { self.prog_value |= @as(u128, 1) << self.bit_cursor; - self.prog_value &= self.prog_config.bit_width.mask(); + self.prog_value &= self.prog_config.int_type.width.mask(); if (self.bit_cursor > 0) self.bit_cursor -= 1; } }, @@ -898,7 +897,7 @@ pub const App = struct { if (char >= '0' and char <= '9') { // Operate on the in-width portion so hidden upper bits do // not corrupt the arithmetic; the edit commits to width. - const m = self.prog_config.bit_width.mask(); + const m = self.prog_config.int_type.width.mask(); self.prog_value = (((self.prog_value & m) *% 10) +% (char - '0')) & m; } }, @@ -913,15 +912,15 @@ pub const App = struct { /// upper bits. The display masks to width and shows a warning while the /// value does not fit. Explicit value edits still commit to width. fn cycleBitWidth(self: *App) void { - self.prog_config.bit_width = switch (self.prog_config.bit_width) { + self.prog_config.int_type.width = switch (self.prog_config.int_type.width) { .bits8 => .bits16, .bits16 => .bits32, .bits32 => .bits64, .bits64 => .bits128, .bits128 => .bits8, }; - if (self.bit_cursor >= self.prog_config.bit_width.bits()) { - self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1); + if (self.bit_cursor >= self.prog_config.int_type.width.bits()) { + self.bit_cursor = @intCast(self.prog_config.int_type.width.bits() - 1); } } @@ -933,7 +932,7 @@ pub const App = struct { } fn toggleSignedness(self: *App) void { - self.prog_config.signedness = switch (self.prog_config.signedness) { + self.prog_config.int_type.signedness = switch (self.prog_config.int_type.signedness) { .signed => .unsigned, .unsigned => .signed, }; @@ -949,12 +948,12 @@ pub const App = struct { /// Snap the bit width to match the active float format (f32 -> 32, f64 -> 64). fn syncFloatWidth(self: *App) void { - self.prog_config.bit_width = switch (self.float_format) { + self.prog_config.int_type.width = switch (self.float_format) { .f32 => .bits32, .f64 => .bits64, }; - if (self.bit_cursor >= self.prog_config.bit_width.bits()) { - self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1); + if (self.bit_cursor >= self.prog_config.int_type.width.bits()) { + self.bit_cursor = @intCast(self.prog_config.int_type.width.bits() - 1); } } @@ -1046,7 +1045,7 @@ pub const App = struct { as_float < 340282366920938463463374607431768211456.0) { const int_val: u128 = @intFromFloat(as_float); - const bw = engine.types.BitWidth.smallestFor(int_val); + const bw = engine.formatter.displayWidthFor(int_val); var hex_buf: [256]u8 = undefined; var oct_buf: [256]u8 = undefined; var bin_buf: [512]u8 = undefined; @@ -1408,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.types.errorPhrase`; the prefix is added at +/// The phrases live once, in `engine.errors.errorPhrase`; 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 { return switch (err) { - inline else => |e| comptime "error: " ++ engine.types.errorPhrase(e), + inline else => |e| comptime "error: " ++ engine.errors.errorPhrase(e), }; } @@ -1770,9 +1769,9 @@ test "other modes still get their own keys after financial mode was added" { // Programmer mode's bit-width cycle still works. app.setMode(.programmer); - const width_before = app.prog_config.bit_width; + const width_before = app.prog_config.int_type.width; try press(&app, &ctx, .{ .codepoint = 'w', .mods = .{ .ctrl = true } }); - try testing.expect(app.prog_config.bit_width != width_before); + try testing.expect(app.prog_config.int_type.width != width_before); } test "shift-tab walks back through the modes" { @@ -2024,7 +2023,7 @@ test "render: programmer mode warns when the value exceeds the display width" { defer app.deinit(); app.setMode(.programmer); app.prog_value = 0xDEADBEEF; - app.prog_config.bit_width = .bits8; + app.prog_config.int_type.width = .bits8; const rows = try renderApp(arena, &app, 100, 30); try testing.expect(test_render.contains(rows, "value exceeds 8 bits")); @@ -2041,11 +2040,11 @@ test "render: programmer mode is well formed at every width and setting" { app.prog_value = 0xFEDCBA9876543210; for ([_]engine.BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |width| { - app.prog_config.bit_width = width; - for ([_]engine.types.Endianness{ .little, .big }) |endian| { + app.prog_config.int_type.width = width; + for ([_]std.builtin.Endian{ .little, .big }) |endian| { app.prog_config.display_endian = endian; - for ([_]engine.types.Signedness{ .signed, .unsigned }) |signedness| { - app.prog_config.signedness = signedness; + for ([_]engine.Integer.Signedness{ .signed, .unsigned }) |signedness| { + app.prog_config.int_type.signedness = signedness; const rows = try renderApp(arena, &app, 100, 40); try testing.expect(test_render.furniture(rows).intact()); var buf: [24]u8 = undefined; @@ -2066,7 +2065,7 @@ test "render: the float view decodes a known bit pattern" { app.setMode(.programmer); app.float_view_active = true; app.float_format = .f32; - app.prog_config.bit_width = .bits32; + app.prog_config.int_type.width = .bits32; app.prog_value = @as(u32, @bitCast(@as(f32, 1.0))); const rows = try renderApp(arena, &app, 100, 30); @@ -2107,7 +2106,7 @@ test "render: the float view decodes every classification by name" { }; for (cases) |case| { app.float_format = case.format; - app.prog_config.bit_width = if (case.format == .f32) .bits32 else .bits64; + app.prog_config.int_type.width = if (case.format == .f32) .bits32 else .bits64; app.prog_value = case.bits; const rows = try renderApp(arena, &app, 100, 34); try testing.expect(test_render.furniture(rows).intact()); @@ -2348,7 +2347,7 @@ test "render: 128-bit programmer mode keeps its input line on a short terminal" var app = testApp(); defer app.deinit(); app.setMode(.programmer); - app.prog_config.bit_width = .bits128; + app.prog_config.int_type.width = .bits128; // Four grid rows plus six base rows do not fit in 16 rows. The view used to // draw them anyway, putting the BIN row on the prompt. @@ -2556,12 +2555,12 @@ test "programmer mode: every clickable control acts" { try app.applyAction(&ctx, .{ .prog_field = .{ .field = .oct, .bit = null } }); try testing.expectEqual(App.ProgField.oct, app.prog_field); // An out-of-width bit is ignored rather than moving the cursor off the value. - app.prog_config.bit_width = .bits8; + app.prog_config.int_type.width = .bits8; try app.applyAction(&ctx, .{ .prog_field = .{ .field = .bits, .bit = 100 } }); try testing.expect(app.bit_cursor < 8); // Toggling bits. - app.prog_config.bit_width = .bits32; + app.prog_config.int_type.width = .bits32; app.prog_value = 0; try app.applyAction(&ctx, .{ .toggle_bit = 3 }); try testing.expectEqual(@as(u128, 8), app.prog_value); @@ -2571,15 +2570,15 @@ test "programmer mode: every clickable control acts" { try testing.expectEqual(@as(u128, 0), app.prog_value); // Settings. - const width_before = app.prog_config.bit_width; + const width_before = app.prog_config.int_type.width; try app.applyAction(&ctx, .cycle_width); - try testing.expect(app.prog_config.bit_width != width_before); + try testing.expect(app.prog_config.int_type.width != width_before); const endian_before = app.prog_config.display_endian; try app.applyAction(&ctx, .toggle_endian); try testing.expect(app.prog_config.display_endian != endian_before); - const signed_before = app.prog_config.signedness; + const signed_before = app.prog_config.int_type.signedness; try app.applyAction(&ctx, .toggle_signedness); - try testing.expect(app.prog_config.signedness != signed_before); + try testing.expect(app.prog_config.int_type.signedness != signed_before); // Float overlay and its format toggle. try app.applyAction(&ctx, .toggle_float); @@ -2606,15 +2605,15 @@ test "programmer mode: bit width cycles through every size and keeps the cursor app.setMode(.programmer); app.value_zone_active = true; app.bit_cursor = 100; - app.prog_config.bit_width = .bits8; + app.prog_config.int_type.width = .bits8; var seen: usize = 0; while (seen < 6) : (seen += 1) { try press(&app, &ctx, .{ .codepoint = 'w', .mods = .{ .ctrl = true } }); - try testing.expect(app.bit_cursor < app.prog_config.bit_width.bits()); + try testing.expect(app.bit_cursor < app.prog_config.int_type.width.bits()); } // Six steps through five widths lands one past the start: 8, 16, 32, 64, 128, 8, 16. - try testing.expectEqual(engine.BitWidth.bits16, app.prog_config.bit_width); + try testing.expectEqual(engine.BitWidth.bits16, app.prog_config.int_type.width); } test "programmer mode: typing edits the focused field in its own base" { @@ -2624,7 +2623,7 @@ test "programmer mode: typing edits the focused field in its own base" { defer ctx.cmds.deinit(testing.allocator); app.setMode(.programmer); app.value_zone_active = true; - app.prog_config.bit_width = .bits32; + app.prog_config.int_type.width = .bits32; // Hex nibble entry. app.prog_field = .hex; @@ -2669,7 +2668,7 @@ test "programmer mode: arrows and space navigate the bit grid" { defer ctx.cmds.deinit(testing.allocator); app.setMode(.programmer); app.value_zone_active = true; - app.prog_config.bit_width = .bits64; + app.prog_config.int_type.width = .bits64; app.prog_field = .bits; app.bit_cursor = 0; @@ -2737,7 +2736,7 @@ test "float view: typing a decimal stores the nearest bit pattern" { app.setMode(.programmer); app.float_view_active = true; app.float_format = .f64; - app.prog_config.bit_width = .bits64; + app.prog_config.int_type.width = .bits64; try app.input.insertSliceAtCursor("3.14"); try press(&app, &ctx, .{ .codepoint = vaxis.Key.enter }); @@ -2752,7 +2751,7 @@ test "float view: typing a decimal stores the nearest bit pattern" { // In the float overlay Ctrl-W swaps format instead of cycling width. try press(&app, &ctx, .{ .codepoint = 'w', .mods = .{ .ctrl = true } }); try testing.expectEqual(engine.FloatFormat.f32, app.float_format); - try testing.expectEqual(engine.BitWidth.bits32, app.prog_config.bit_width); + try testing.expectEqual(engine.BitWidth.bits32, app.prog_config.int_type.width); // Ctrl-F leaves the overlay. try press(&app, &ctx, .{ .codepoint = 'f', .mods = .{ .ctrl = true } }); @@ -3025,7 +3024,7 @@ test "render: programmer mode draws the cursor in whichever field is focused" { for (fields) |field| { app.prog_field = field; for ([_]engine.BitWidth{ .bits8, .bits64, .bits128 }) |width| { - app.prog_config.bit_width = width; + app.prog_config.int_type.width = width; app.bit_cursor = @intCast(@min(5, width.bits() - 1)); const rows = try renderApp(arena, &app, 110, 40); try testing.expect(test_render.furniture(rows).intact()); diff --git a/src/tui/financial.zig b/src/tui/financial.zig index d965c9a..3f8759d 100644 --- a/src/tui/financial.zig +++ b/src/tui/financial.zig @@ -870,7 +870,7 @@ 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.types.errorPhrase`, so this is no longer a third copy of +/// rest defer to `engine.errors.errorPhrase`, 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 { @@ -878,7 +878,7 @@ pub fn errorText(err: engine.CalcError) []const u8 { 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.types.errorPhrase(err), + else => engine.errors.errorPhrase(err), }; } diff --git a/src/tui/float_view.zig b/src/tui/float_view.zig index f39bb26..0af6d4f 100644 --- a/src/tui/float_view.zig +++ b/src/tui/float_view.zig @@ -37,7 +37,7 @@ pub fn drawFloatView(app: *tui.App, surface: *vxfw.Surface, width: u16, height: _ = width; const format = app.float_format; const total = format.totalBits(); - const bw = app.prog_config.bit_width; + const bw = app.prog_config.int_type.width; const bits: u64 = @truncate(app.prog_value & bw.mask()); const info = fi.decompose(format, bits); diff --git a/src/tui/programmer.zig b/src/tui/programmer.zig index 470742e..bc9ae80 100644 --- a/src/tui/programmer.zig +++ b/src/tui/programmer.zig @@ -9,7 +9,7 @@ const tui = @import("../tui.zig"); const C = draw.C; pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, height: u16) void { - const bw = app.prog_config.bit_width; + const bw = app.prog_config.int_type.width; const val = app.prog_value & bw.mask(); const focused = app.prog_field; @@ -17,7 +17,7 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei var config_buf: [80]u8 = undefined; const config_str = std.fmt.bufPrint(&config_buf, "Bits: {d} Signed: {s} Endian: {s}", .{ bw.bits(), - if (app.prog_config.signedness == .signed) "yes" else "no", + if (app.prog_config.int_type.signedness == .signed) "yes" else "no", if (app.prog_config.display_endian == .little) "LE" else "BE", }) catch "Bits: ??"; draw.writeStr(surface, 2, 2, config_str, .{ .fg = C.muted }); @@ -65,7 +65,7 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei drawBitGrid(app, surface, grid_start, val, bw); - const int = engine.types.Integer{ .raw = val, .bit_width = bw, .signedness = .signed }; + const int = engine.Integer.init(val, .{ .width = bw, .signedness = .signed }); // DEC(s) var sdec_buf: [256]u8 = undefined; @@ -282,7 +282,7 @@ fn drawFieldWithCursor(surface: *vxfw.Surface, row: u16, col: u16, text: []const } } -fn drawBitGrid(app: *tui.App, surface: *vxfw.Surface, start_row: u16, val: u128, bw: engine.types.BitWidth) void { +fn drawBitGrid(app: *tui.App, surface: *vxfw.Surface, start_row: u16, val: u128, bw: engine.Integer.BitWidth) void { const width_bits = bw.bits(); const bits_per_row: u8 = if (width_bits > 32) 32 else width_bits; const num_rows: u16 = (@as(u16, width_bits) + bits_per_row - 1) / bits_per_row;