begin human review - rename Integer, remove types.zig
This commit is contained in:
parent
7121dd9dea
commit
cb7da0fe9e
21 changed files with 800 additions and 646 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
179
engine/src/Integer.zig
Normal file
179
engine/src/Integer.zig
Normal file
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
136
engine/src/errors.zig
Normal file
136
engine/src/errors.zig
Normal file
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
95
src/main.zig
95
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);
|
||||
|
|
|
|||
111
src/tui.zig
111
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());
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue