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 |
|
| 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 |
|
| `ast.zig` | AST node definitions |
|
||||||
|
| `parser.zig` | Pratt parser -> AST |
|
||||||
| `evaluator.zig` | Walk AST, produce results |
|
| `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 |
|
| `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 |
|
| `units.zig` | Unit conversion tables and resolver |
|
||||||
| `financial.zig` | CAGR, TVM, compound interest, amortization |
|
| `financial.zig` | CAGR, TVM, compound interest, amortization |
|
||||||
| `types.zig` | Shared types (Value, Error, etc.) |
|
| `engine.zig` | Public API surface (Zig-native). Imports and re-exports only; defines nothing |
|
||||||
| `engine.zig` | Public API surface (Zig-native) |
|
|
||||||
| `c_api.zig` | `extern "C"` wrappers for JNI/FFI consumers |
|
| `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
|
### 2.2 Core Data Types
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
|
|
@ -212,23 +223,44 @@ The evaluator maintains an `Environment`:
|
||||||
```zig
|
```zig
|
||||||
pub const Environment = struct {
|
pub const Environment = struct {
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
mode: Mode,
|
|
||||||
/// Variables hold `Number`, so an assignment keeps whatever exactness its
|
/// Variables hold `Number`, so an assignment keeps whatever exactness its
|
||||||
/// expression had: `X = 0.1` stores exactly one tenth.
|
/// expression had: `X = 0.1` stores exactly one tenth.
|
||||||
variables: std.StringHashMap(Number),
|
variables: std.StringHashMap(Number),
|
||||||
ans: ?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,
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
### 2.6 Number Display Formatting
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -756,6 +756,50 @@ Remaining subcommands deferred until their engine modules exist.
|
||||||
- NOT DONE: mouse wheel scrolling for history
|
- NOT DONE: mouse wheel scrolling for history
|
||||||
- Verify: help overlay works, mouse interactions work, looks reasonable in 80x24 terminal
|
- 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
|
### 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,
|
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
|
//! deallocation"), which is how both eval entry points ended up leaking the whole
|
||||||
//! tree on every call.
|
//! tree on every call.
|
||||||
|
|
||||||
const types = @import("types.zig");
|
const Base = @import("tokenizer.zig").Base;
|
||||||
const Base = types.Base;
|
|
||||||
|
|
||||||
/// A single expression node.
|
/// A single expression node.
|
||||||
pub const Expr = union(enum) {
|
pub const Expr = union(enum) {
|
||||||
|
|
|
||||||
|
|
@ -15,21 +15,21 @@
|
||||||
//! `~`.
|
//! `~`.
|
||||||
//!
|
//!
|
||||||
//! FR-2.12 promises that every operator means the same thing in both modes, so
|
//! 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
|
//! signedness). Standard mode is fixed at 64-bit signed; a different width is what
|
||||||
//! programmer mode is for (FR-2.3).
|
//! programmer mode is for (FR-2.3).
|
||||||
//!
|
//!
|
||||||
//! Values are carried as a `u128` holding the two's complement bit pattern masked
|
//! 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 std = @import("std");
|
||||||
const ast = @import("ast.zig");
|
const ast = @import("ast.zig");
|
||||||
const BinaryOp = ast.BinaryOp;
|
const BinaryOp = ast.BinaryOp;
|
||||||
const types = @import("types.zig");
|
const Integer = @import("Integer.zig");
|
||||||
const BitWidth = types.BitWidth;
|
const BitWidth = Integer.BitWidth;
|
||||||
const Signedness = types.Signedness;
|
const Signedness = Integer.Signedness;
|
||||||
const ProgrammerConfig = types.ProgrammerConfig;
|
const IntType = Integer.IntType;
|
||||||
const CalcError = types.CalcError;
|
const CalcError = @import("errors.zig").CalcError;
|
||||||
|
|
||||||
/// The operators this module implements.
|
/// 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.
|
/// How far a shift moves, once the distance has been checked.
|
||||||
const Distance = union(enum) {
|
const Distance = union(enum) {
|
||||||
/// Shorter than the width, so some bits survive.
|
/// 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
|
/// 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`.
|
/// mode used to reduce it modulo 64, so `8 >> -1` quietly became `8 >> 63`.
|
||||||
fn distance(domain: Domain, right: u128) CalcError!Distance {
|
fn distance(int_type: IntType, right: u128) CalcError!Distance {
|
||||||
if (domain.isNegative(right)) return CalcError.DomainError;
|
if (int_type.isNegative(right)) return CalcError.DomainError;
|
||||||
const value = right & domain.mask();
|
const value = right & int_type.mask();
|
||||||
if (value >= domain.bits()) return .past_width;
|
if (value >= int_type.bits()) return .past_width;
|
||||||
return .{ .within = @intCast(value) };
|
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
|
/// 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.
|
/// saturated: rotating a 64-bit value by 65 is rotating it by 1.
|
||||||
fn rotation(domain: Domain, right: u128) CalcError!u7 {
|
fn rotation(int_type: IntType, right: u128) CalcError!u7 {
|
||||||
if (domain.isNegative(right)) return CalcError.DomainError;
|
if (int_type.isNegative(right)) return CalcError.DomainError;
|
||||||
return @intCast((right & domain.mask()) % domain.bits());
|
return @intCast((right & int_type.mask()) % int_type.bits());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a fixed-width operation. Operands and result are masked bit patterns.
|
/// 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 {
|
pub fn apply(int_type: IntType, op: Op, left_in: u128, right_in: u128) CalcError!u128 {
|
||||||
const mask = domain.mask();
|
const mask = int_type.mask();
|
||||||
const left = left_in & mask;
|
const left = left_in & mask;
|
||||||
const right = right_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,
|
.bit_xor => left ^ right,
|
||||||
// Bits shifted past the end of the width are discarded, not wrapped:
|
// Bits shifted past the end of the width are discarded, not wrapped:
|
||||||
// `0b1000 << 1` is 16. Wrapping is what `rol` and `ror` are for.
|
// `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,
|
.past_width => 0,
|
||||||
.within => |amt| (left << amt) & mask,
|
.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,
|
.past_width => 0,
|
||||||
.within => |amt| (left >> amt) & mask,
|
.within => |amt| (left >> amt) & mask,
|
||||||
},
|
},
|
||||||
.shift_right => blk: {
|
.shift_right => blk: {
|
||||||
const negative = domain.isNegative(left);
|
const negative = int_type.isNegative(left);
|
||||||
switch (try distance(domain, right)) {
|
switch (try distance(int_type, right)) {
|
||||||
// Shifting a negative value all the way out leaves the sign fill,
|
// Shifting a negative value all the way out leaves the sign fill,
|
||||||
// which is every bit set; a non-negative one leaves zero.
|
// which is every bit set; a non-negative one leaves zero.
|
||||||
.past_width => break :blk if (negative) mask else 0,
|
.past_width => break :blk if (negative) mask else 0,
|
||||||
.within => |amt| {
|
.within => |amt| {
|
||||||
if (!negative) break :blk (left >> amt) & mask;
|
if (!negative) break :blk (left >> amt) & mask;
|
||||||
// Shift in the signed domain so the sign bit is the fill.
|
// 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;
|
break :blk @as(u128, @bitCast(extended >> amt)) & mask;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.rotate_left => blk: {
|
.rotate_left => blk: {
|
||||||
const amt = try rotation(domain, right);
|
const amt = try rotation(int_type, right);
|
||||||
if (amt == 0) break :blk left;
|
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;
|
break :blk ((left << amt) | (left >> anti)) & mask;
|
||||||
},
|
},
|
||||||
.rotate_right => blk: {
|
.rotate_right => blk: {
|
||||||
const amt = try rotation(domain, right);
|
const amt = try rotation(int_type, right);
|
||||||
if (amt == 0) break :blk left;
|
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;
|
break :blk ((left >> amt) | (left << anti)) & mask;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bitwise complement within the width.
|
/// Bitwise complement within the width.
|
||||||
pub fn not(domain: Domain, value: u128) u128 {
|
pub fn not(int_type: IntType, value: u128) u128 {
|
||||||
return ~value & domain.mask();
|
return ~value & int_type.mask();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Two's complement negation within the width.
|
/// Two's complement negation within the width.
|
||||||
pub fn negate(domain: Domain, value: u128) u128 {
|
pub fn negate(int_type: IntType, value: u128) u128 {
|
||||||
return (~value +% 1) & domain.mask();
|
return (~value +% 1) & int_type.mask();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Tests --
|
// -- Tests --
|
||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
const w8: Domain = .{ .bit_width = .bits8, .signedness = .signed };
|
const i8_type: IntType = .{ .width = .bits8 };
|
||||||
const u8_domain: Domain = .{ .bit_width = .bits8, .signedness = .unsigned };
|
const u8_type: IntType = .{ .width = .bits8, .signedness = .unsigned };
|
||||||
|
|
||||||
test "fromBinaryOp: every fixed-width operator maps, no arithmetic one does" {
|
test "fromBinaryOp: every fixed-width operator maps, no arithmetic one does" {
|
||||||
// The compiler enforces the total mapping; this pins which side each lands on.
|
// 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" {
|
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.
|
// -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(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" {
|
test "logical right shift fills with zeros" {
|
||||||
// The same bits, shifted the other way: 0b0111_1100 is 124.
|
// 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);
|
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;
|
var amt: u128 = 0;
|
||||||
while (amt < 8) : (amt += 1) {
|
while (amt < 8) : (amt += 1) {
|
||||||
try testing.expectEqual(
|
try testing.expectEqual(
|
||||||
try apply(w8, .shift_right, value, amt),
|
try apply(i8_type, .shift_right, value, amt),
|
||||||
try apply(w8, .shift_right_logical, 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
|
// 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
|
// old programmer-mode implementation looked at the top bit regardless of the
|
||||||
// configured signedness and gave 0xFF.
|
// 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_type, .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_logical, 0xFF, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "shifts run to completion instead of wrapping or clamping the distance" {
|
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;
|
// 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.
|
// programmer mode clamped it to width - 1, so 8-bit `0xFF >>> 20` was 1.
|
||||||
const standard_domain = Domain.standard;
|
const i64_type: IntType = .{};
|
||||||
try testing.expectEqual(@as(u128, 0), try apply(standard_domain, .shift_left, 1, 64));
|
try testing.expectEqual(@as(u128, 0), try apply(i64_type, .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(i64_type, .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(i8_type, .shift_right_logical, 0xFF, 20));
|
||||||
try testing.expectEqual(@as(u128, 0), try apply(w8, .shift_left, 0xFF, 8));
|
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.
|
// 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(i8_type, .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, 100));
|
||||||
// A non-negative one is zero.
|
// 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" {
|
test "shifting by one less than the width still keeps a bit" {
|
||||||
// The boundary the clamping rule used to hide.
|
// 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, 0b1000_0000), try apply(i8_type, .shift_left, 1, 7));
|
||||||
try testing.expectEqual(@as(u128, 1), try apply(w8, .shift_right_logical, 0b1000_0000, 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(w8, .shift_right, 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" {
|
test "a negative shift distance is a domain error, not a huge one" {
|
||||||
const neg_one: u128 = 0xFF; // -1 in 8-bit signed
|
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(i8_type, .shift_left, 1, neg_one));
|
||||||
try testing.expectError(CalcError.DomainError, apply(w8, .shift_right, 1, neg_one));
|
try testing.expectError(CalcError.DomainError, apply(i8_type, .shift_right, 1, neg_one));
|
||||||
try testing.expectError(CalcError.DomainError, apply(w8, .shift_right_logical, 1, neg_one));
|
try testing.expectError(CalcError.DomainError, apply(i8_type, .shift_right_logical, 1, neg_one));
|
||||||
try testing.expectError(CalcError.DomainError, apply(w8, .rotate_left, 1, neg_one));
|
try testing.expectError(CalcError.DomainError, apply(i8_type, .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, .rotate_right, 1, neg_one));
|
||||||
// The same pattern in an unsigned domain is 255, a distance past the width.
|
// 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" {
|
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, 0b0000_0011), try apply(i8_type, .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, 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.
|
// 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, 0b1000_0001), try apply(i8_type, .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, 0b0000_0011), try apply(i8_type, .rotate_left, 0b1000_0001, 9));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "rotate left and rotate right are inverses at every distance and width" {
|
test "rotate left and rotate right are inverses at every distance and width" {
|
||||||
for ([_]BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |bw| {
|
for ([_]BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |bw| {
|
||||||
const domain: Domain = .{ .bit_width = bw, .signedness = .unsigned };
|
const int_type: IntType = .{ .width = bw, .signedness = .unsigned };
|
||||||
const value: u128 = 0x1234_5678_9ABC_DEF0 & domain.mask();
|
const value: u128 = 0x1234_5678_9ABC_DEF0 & int_type.mask();
|
||||||
var amt: u128 = 0;
|
var amt: u128 = 0;
|
||||||
while (amt < domain.bits()) : (amt += 1) {
|
while (amt < int_type.bits()) : (amt += 1) {
|
||||||
const there = try apply(domain, .rotate_left, value, amt);
|
const there = try apply(int_type, .rotate_left, value, amt);
|
||||||
const back = try apply(domain, .rotate_right, there, amt);
|
const back = try apply(int_type, .rotate_right, there, amt);
|
||||||
try testing.expectEqual(value, back);
|
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" {
|
test "results stay inside the width" {
|
||||||
for ([_]BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |bw| {
|
for ([_]BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |bw| {
|
||||||
const domain: Domain = .{ .bit_width = bw, .signedness = .signed };
|
const int_type: IntType = .{ .width = bw, .signedness = .signed };
|
||||||
const all_ones = domain.mask();
|
const all_ones = int_type.mask();
|
||||||
inline for (@typeInfo(Op).@"enum".fields) |field| {
|
inline for (@typeInfo(Op).@"enum".fields) |field| {
|
||||||
const op = @field(Op, field.name);
|
const op = @field(Op, field.name);
|
||||||
// 1 is a safe distance for the shifts and a legal operand for the rest.
|
// 1 is a safe distance for the shifts and a legal operand for the rest.
|
||||||
const result = try apply(domain, op, all_ones, 1);
|
const result = try apply(int_type, op, all_ones, 1);
|
||||||
try testing.expectEqual(result, result & domain.mask());
|
try testing.expectEqual(result, result & int_type.mask());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "not and negate stay inside the width" {
|
test "not and negate stay inside the width" {
|
||||||
try testing.expectEqual(@as(u128, 0xFF), not(w8, 0));
|
try testing.expectEqual(@as(u128, 0xFF), not(i8_type, 0));
|
||||||
try testing.expectEqual(@as(u128, 0), not(w8, 0xFF));
|
try testing.expectEqual(@as(u128, 0), not(i8_type, 0xFF));
|
||||||
try testing.expectEqual(@as(u128, 0xFF), negate(w8, 1));
|
try testing.expectEqual(@as(u128, 0xFF), negate(i8_type, 1));
|
||||||
try testing.expectEqual(@as(u128, 1), negate(w8, 0xFF));
|
try testing.expectEqual(@as(u128, 1), negate(i8_type, 0xFF));
|
||||||
// Negating the most negative value gives itself back, as two's complement does.
|
// 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" {
|
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, -1), i8_type.signExtend(0xFF));
|
||||||
try testing.expectEqual(@as(i128, 255), u8_domain.signExtend(0xFF));
|
try testing.expectEqual(@as(i128, 255), u8_type.signExtend(0xFF));
|
||||||
try testing.expectEqual(@as(i128, -128), w8.signExtend(0x80));
|
try testing.expectEqual(@as(i128, -128), i8_type.signExtend(0x80));
|
||||||
try testing.expectEqual(@as(i128, 127), w8.signExtend(0x7F));
|
try testing.expectEqual(@as(i128, 127), i8_type.signExtend(0x7F));
|
||||||
// A 128-bit domain has no bits above the width to fill.
|
// A 128-bit domain has no bits above the width to fill.
|
||||||
const w128: Domain = .{ .bit_width = .bits128, .signedness = .signed };
|
const i128_type: IntType = .{ .width = .bits128 };
|
||||||
try testing.expectEqual(@as(i128, -1), w128.signExtend(w128.mask()));
|
try testing.expectEqual(@as(i128, -1), i128_type.signExtend(i128_type.mask()));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "standard mode is 64-bit signed" {
|
test "the default type, which standard mode uses, is 64-bit signed" {
|
||||||
try testing.expectEqual(BitWidth.bits64, Domain.standard.bit_width);
|
const i64_type: IntType = .{};
|
||||||
try testing.expectEqual(Signedness.signed, Domain.standard.signedness);
|
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.
|
// -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 minus_eight: u128 = @bitCast(@as(i128, -8) & @as(i128, @bitCast(i64_type.mask())));
|
||||||
const shifted = try apply(Domain.standard, .shift_right, minus_eight, 1);
|
const shifted = try apply(i64_type, .shift_right, minus_eight, 1);
|
||||||
try testing.expectEqual(@as(i128, -4), Domain.standard.signExtend(shifted));
|
try testing.expectEqual(@as(i128, -4), i64_type.signExtend(shifted));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,31 @@
|
||||||
//! Tally calculation engine.
|
//! Tally calculation engine.
|
||||||
//!
|
//!
|
||||||
//! Pure computation library with no I/O. Provides expression parsing,
|
//! Pure computation library with no I/O. Provides expression parsing, evaluation,
|
||||||
//! evaluation, programmer-mode bit manipulation, struct layout computation,
|
//! programmer-mode bit manipulation, unit conversion, and financial calculations.
|
||||||
//! 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 tokenizer = @import("tokenizer.zig");
|
||||||
pub const ast = @import("ast.zig");
|
pub const ast = @import("ast.zig");
|
||||||
pub const parser = @import("parser.zig");
|
pub const parser = @import("parser.zig");
|
||||||
pub const evaluator = @import("evaluator.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 bitwise = @import("bitwise.zig");
|
||||||
|
pub const programmer = @import("programmer.zig");
|
||||||
|
// Domains and display.
|
||||||
pub const formatter = @import("formatter.zig");
|
pub const formatter = @import("formatter.zig");
|
||||||
pub const float_interp = @import("float_interp.zig");
|
pub const float_interp = @import("float_interp.zig");
|
||||||
pub const units = @import("units.zig");
|
pub const units = @import("units.zig");
|
||||||
pub const financial = @import("financial.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`
|
// 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
|
// 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
|
// 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
|
// of it had no callers, and `Value` was re-exported after the type it named had
|
||||||
// stopped being the engine's result type.
|
// stopped being the engine's result type.
|
||||||
pub const Mode = types.Mode;
|
pub const CalcError = errors.CalcError;
|
||||||
pub const BitWidth = types.BitWidth;
|
pub const BitWidth = Integer.BitWidth;
|
||||||
pub const CalcError = types.CalcError;
|
|
||||||
pub const Environment = evaluator.Environment;
|
pub const Environment = evaluator.Environment;
|
||||||
pub const evalString = evaluator.evalString;
|
pub const evalString = evaluator.evalString;
|
||||||
pub const evalStringInfo = evaluator.evalStringInfo;
|
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 ast = @import("ast.zig");
|
||||||
const Expr = ast.Expr;
|
const Expr = ast.Expr;
|
||||||
const BinaryOp = ast.BinaryOp;
|
const BinaryOp = ast.BinaryOp;
|
||||||
const types = @import("types.zig");
|
const Integer = @import("Integer.zig");
|
||||||
const Mode = types.Mode;
|
const IntType = Integer.IntType;
|
||||||
const ProgrammerConfig = types.ProgrammerConfig;
|
const CalcError = @import("errors.zig").CalcError;
|
||||||
const CalcError = types.CalcError;
|
|
||||||
const parser_mod = @import("parser.zig");
|
const parser_mod = @import("parser.zig");
|
||||||
const Parser = parser_mod.Parser;
|
const Parser = parser_mod.Parser;
|
||||||
const number_mod = @import("number.zig");
|
const number_mod = @import("number.zig");
|
||||||
|
|
@ -22,23 +21,24 @@ const Number = number_mod.Number;
|
||||||
const bitwise = @import("bitwise.zig");
|
const bitwise = @import("bitwise.zig");
|
||||||
const financial = @import("financial.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
|
/// 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
|
/// exactness its expression had: `X = 0.1` stores exactly one tenth rather than
|
||||||
/// a binary approximation of it.
|
/// 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 {
|
pub const Environment = struct {
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
mode: Mode,
|
|
||||||
programmer_config: ProgrammerConfig,
|
|
||||||
variables: std.StringHashMap(Number),
|
variables: std.StringHashMap(Number),
|
||||||
ans: Number,
|
ans: Number,
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, mode: Mode) Environment {
|
pub fn init(allocator: Allocator) Environment {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.mode = mode,
|
|
||||||
.programmer_config = .{},
|
|
||||||
.variables = std.StringHashMap(Number).init(allocator),
|
.variables = std.StringHashMap(Number).init(allocator),
|
||||||
// Starts inexact so that `init` cannot fail; the first evaluation
|
// Starts inexact so that `init` cannot fail; the first evaluation
|
||||||
// replaces it.
|
// replaces it.
|
||||||
|
|
@ -173,7 +173,7 @@ fn evalExact(env: *Environment, scratch: Allocator, expr: *const Expr) CalcError
|
||||||
// setting entirely.
|
// setting entirely.
|
||||||
.bitwise_not => blk: {
|
.bitwise_not => blk: {
|
||||||
const bits = try toFixedWidthBits(operand.toFloat(scratch));
|
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 l = try toFixedWidthBits(left.toFloat(scratch));
|
||||||
const r = try toFixedWidthBits(right.toFloat(scratch));
|
const r = try toFixedWidthBits(right.toFloat(scratch));
|
||||||
const result = try bitwise.apply(
|
const result = try bitwise.apply(
|
||||||
standard_domain,
|
standard_int_type,
|
||||||
comptime bitwise.fromBinaryOp(fixed_op).?,
|
comptime bitwise.fromBinaryOp(fixed_op).?,
|
||||||
l,
|
l,
|
||||||
r,
|
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
|
/// 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
|
/// is what programmer mode is for, and pretending otherwise is how `~` came to
|
||||||
/// honour the setting while the shifts beside it did not.
|
/// 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.
|
/// 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.
|
/// Read a result pattern back as a number, signed, since standard mode is signed.
|
||||||
fn fromFixedWidthBits(bits: u128) Number {
|
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.
|
/// 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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
var env = Environment.init(alloc, .standard);
|
var env = Environment.init(alloc);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
const result = try evalString(&env, alloc, source);
|
const result = try evalString(&env, alloc, source);
|
||||||
return result.toFloat(alloc);
|
return result.toFloat(alloc);
|
||||||
|
|
@ -595,7 +596,7 @@ fn testEvalProgrammer(source: []const u8) !f64 {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
var env = Environment.init(alloc, .programmer);
|
var env = Environment.init(alloc);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
const result = try evalString(&env, alloc, source);
|
const result = try evalString(&env, alloc, source);
|
||||||
return result.toFloat(alloc);
|
return result.toFloat(alloc);
|
||||||
|
|
@ -765,7 +766,7 @@ test "eval variable assignment and use" {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
var env = Environment.init(alloc, .standard);
|
var env = Environment.init(alloc);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
|
|
||||||
const assign_result = try evalString(&env, alloc, "X = 42");
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
var env = Environment.init(alloc, .standard);
|
var env = Environment.init(alloc);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
|
|
||||||
_ = try evalString(&env, alloc, "7 * 6");
|
_ = 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| {
|
for (shared) |source| {
|
||||||
const standard = try testEval(source);
|
const standard = try testEval(source);
|
||||||
const prog = try programmer_mod.evalProgrammerString(alloc, source, .{
|
const prog = try programmer_mod.evalProgrammerString(alloc, source, .{
|
||||||
.bit_width = .bits64,
|
.int_type = .{ .width = .bits64, .signedness = .signed },
|
||||||
.signedness = .signed,
|
|
||||||
});
|
});
|
||||||
try testing.expectEqual(@as(i128, @intFromFloat(standard)), prog.signedValue());
|
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"));
|
try testing.expectEqual(@as(f64, 1.0), try testEval("1 ror 64"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "standard mode: the programmer width setting does not reach it" {
|
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
|
// `~` 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
|
// ignored it, so `--bits 8` changed one operator and not the others. The
|
||||||
// fixed at 64-bit signed.
|
// environment no longer carries a width to disagree about: there is nothing to
|
||||||
var env = Environment.init(testing.allocator, .standard);
|
// set here, which is the point.
|
||||||
|
var env = Environment.init(testing.allocator);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
env.programmer_config.bit_width = .bits8;
|
|
||||||
|
|
||||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||||
defer arena.deinit();
|
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");
|
var shifted = try evalString(&env, alloc, "1 << 10");
|
||||||
defer shifted.deinit();
|
defer shifted.deinit();
|
||||||
try testing.expectEqual(@as(f64, 1024.0), shifted.toFloat(alloc));
|
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" {
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
var env = Environment.init(alloc, .standard);
|
var env = Environment.init(alloc);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
const info = try evalStringInfo(&env, alloc, "0o777 - 0x0f");
|
const info = try evalStringInfo(&env, alloc, "0o777 - 0x0f");
|
||||||
try testing.expectEqual(@as(f64, 496.0), info.value.toFloat(alloc));
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
var env = Environment.init(alloc, .standard);
|
var env = Environment.init(alloc);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
const info = try evalStringInfo(&env, alloc, "2 + 2");
|
const info = try evalStringInfo(&env, alloc, "2 + 2");
|
||||||
try testing.expectEqual(@as(f64, 4.0), info.value.toFloat(alloc));
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
var env = Environment.init(alloc, .standard);
|
var env = Environment.init(alloc);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
const info = try evalStringInfo(&env, alloc, "sqrt(0b100) + 1");
|
const info = try evalStringInfo(&env, alloc, "sqrt(0b100) + 1");
|
||||||
try testing.expect(info.has_nondecimal_literal);
|
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.
|
/// Evaluate and keep the exact result. The arena owns everything.
|
||||||
fn testEvalNumber(arena: *std.heap.ArenaAllocator, source: []const u8) !Number {
|
fn testEvalNumber(arena: *std.heap.ArenaAllocator, source: []const u8) !Number {
|
||||||
const a = arena.allocator();
|
const a = arena.allocator();
|
||||||
var env = Environment.init(a, .standard);
|
var env = Environment.init(a);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
return evalString(&env, a, source);
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const a = arena.allocator();
|
const a = arena.allocator();
|
||||||
var env = Environment.init(a, .standard);
|
var env = Environment.init(a);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
|
|
||||||
const assigned = try evalString(&env, a, "X = 0.1");
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const a = arena.allocator();
|
const a = arena.allocator();
|
||||||
var env = Environment.init(a, .standard);
|
var env = Environment.init(a);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
|
|
||||||
_ = try evalString(&env, a, "1/3");
|
_ = 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" {
|
test "Number API: reassigning a variable releases the old value" {
|
||||||
// Exercises the replace path in setVar, which must deinit the previous
|
// Exercises the replace path in setVar, which must deinit the previous
|
||||||
// Number rather than leaking it.
|
// Number rather than leaking it.
|
||||||
var env = Environment.init(testing.allocator, .standard);
|
var env = Environment.init(testing.allocator);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
|
|
||||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
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" {
|
test "Number API: a variable name outliving its source text stays valid" {
|
||||||
// setVar duplicates the name because it points into the expression source,
|
// setVar duplicates the name because it points into the expression source,
|
||||||
// which the caller may free (the TUI frees history on Ctrl-L).
|
// 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();
|
defer env.deinit();
|
||||||
|
|
||||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
var env = Environment.init(alloc, .standard);
|
var env = Environment.init(alloc);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
|
|
||||||
const result = try evalString(&env, alloc, "cagr(1000, 2000, 10)");
|
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.
|
// whole parsed tree on every call, which an arena silently absorbs.
|
||||||
|
|
||||||
test "no leak: a successful evaluation releases the parsed tree" {
|
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();
|
defer env.deinit();
|
||||||
|
|
||||||
const sources = [_][]const u8{
|
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" {
|
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();
|
defer env.deinit();
|
||||||
|
|
||||||
const sources = [_][]const u8{
|
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" {
|
test "no leak: repeated evaluation does not accumulate" {
|
||||||
// A long interactive session is the case that made this visible: the TUI
|
// A long interactive session is the case that made this visible: the TUI
|
||||||
// evaluates on every Enter and never frees anything itself.
|
// 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();
|
defer env.deinit();
|
||||||
|
|
||||||
var i: usize = 0;
|
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" {
|
test "a grouped literal past 2^53 is still exact" {
|
||||||
// The separators have to reach the exact re-parse, not just the f64 channel.
|
// 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();
|
defer env.deinit();
|
||||||
var value = try evalString(&env, testing.allocator, "9,007,199,254,740,993");
|
var value = try evalString(&env, testing.allocator, "9,007,199,254,740,993");
|
||||||
defer value.deinit();
|
defer value.deinit();
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const math = std.math;
|
const math = std.math;
|
||||||
const types = @import("types.zig");
|
const CalcError = @import("errors.zig").CalcError;
|
||||||
const CalcError = types.CalcError;
|
|
||||||
|
|
||||||
/// Iteration cap for the rate solver.
|
/// Iteration cap for the rate solver.
|
||||||
pub const max_iterations: usize = 1000;
|
pub const max_iterations: usize = 1000;
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
//! magnitude is below the fractional budget
|
//! magnitude is below the fractional budget
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const types = @import("types.zig");
|
const Integer = @import("Integer.zig");
|
||||||
const BitWidth = types.BitWidth;
|
const BitWidth = Integer.BitWidth;
|
||||||
const Endianness = types.Endianness;
|
const Endianness = std.builtin.Endian;
|
||||||
const Number = @import("number.zig").Number;
|
const Number = @import("number.zig").Number;
|
||||||
|
|
||||||
/// A formatted value with both display and clipboard representations.
|
/// 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;
|
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.
|
/// Format an integer for programmer mode hex display.
|
||||||
/// Display: "FF FF FF FF" (space per byte), byte order per `endian`.
|
/// Display: "FF FF FF FF" (space per byte), byte order per `endian`.
|
||||||
/// Raw: "0xFFFFFFFF" (no separators, canonical MSB-first value regardless of
|
/// Raw: "0xFFFFFFFF" (no separators, canonical MSB-first value regardless of
|
||||||
|
|
@ -624,6 +648,31 @@ test "formatFloat: zero" {
|
||||||
try testing.expectEqualStrings("0", result.raw);
|
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" {
|
test "formatHex: 8-bit" {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
const result = formatHex(&buf, 0xFF, .bits8, .big);
|
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.expectEqual(@as(usize, 4), negative.int_digits);
|
||||||
try testing.expectEqualStrings(".56", negative.tail);
|
try testing.expectEqualStrings(".56", negative.tail);
|
||||||
|
|
||||||
const integer = splitDecimalText("-70");
|
const whole = splitDecimalText("-70");
|
||||||
try testing.expectEqual(@as(usize, 1), integer.sign_len);
|
try testing.expectEqual(@as(usize, 1), whole.sign_len);
|
||||||
try testing.expectEqual(@as(usize, 2), integer.int_digits);
|
try testing.expectEqual(@as(usize, 2), whole.int_digits);
|
||||||
try testing.expectEqualStrings("", integer.tail);
|
try testing.expectEqualStrings("", whole.tail);
|
||||||
|
|
||||||
const explicit_plus = splitDecimalText("+5");
|
const explicit_plus = splitDecimalText("+5");
|
||||||
try testing.expectEqual(@as(usize, 1), explicit_plus.sign_len);
|
try testing.expectEqual(@as(usize, 1), explicit_plus.sign_len);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const rational = @import("rational.zig");
|
const rational = @import("rational.zig");
|
||||||
const Rational = rational.Rational;
|
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.
|
/// 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" {
|
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
|
// 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
|
// 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.
|
// 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 TokenKind = tokenizer_mod.TokenKind;
|
||||||
const Token = tokenizer_mod.Token;
|
const Token = tokenizer_mod.Token;
|
||||||
const parseNumber = tokenizer_mod.parseNumber;
|
const parseNumber = tokenizer_mod.parseNumber;
|
||||||
const types = @import("types.zig");
|
const CalcError = @import("errors.zig").CalcError;
|
||||||
const CalcError = types.CalcError;
|
|
||||||
|
|
||||||
/// Precedence levels (higher = tighter binding).
|
/// Precedence levels (higher = tighter binding).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,47 @@
|
||||||
//! Programmer mode evaluator for Tally.
|
//! Programmer mode evaluator for Tally.
|
||||||
//!
|
//!
|
||||||
//! All operations use exact integer arithmetic (u128 storage), with results
|
//! All operations use exact integer arithmetic (u128 storage), with results masked
|
||||||
//! masked to the configured bit width. No floating-point involved.
|
//! to the configured width. No floating-point involved. The bitwise operators, the
|
||||||
//! Produces Integer values with signed/unsigned interpretation.
|
//! 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 std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ast = @import("ast.zig");
|
const ast = @import("ast.zig");
|
||||||
const Expr = ast.Expr;
|
const Expr = ast.Expr;
|
||||||
const BinaryOp = ast.BinaryOp;
|
const BinaryOp = ast.BinaryOp;
|
||||||
const types = @import("types.zig");
|
const Integer = @import("Integer.zig");
|
||||||
const Integer = types.Integer;
|
const IntType = Integer.IntType;
|
||||||
const ProgrammerConfig = types.ProgrammerConfig;
|
const Endianness = std.builtin.Endian;
|
||||||
const CalcError = types.CalcError;
|
const CalcError = @import("errors.zig").CalcError;
|
||||||
const parser_mod = @import("parser.zig");
|
const parser_mod = @import("parser.zig");
|
||||||
const Parser = parser_mod.Parser;
|
const Parser = parser_mod.Parser;
|
||||||
const bitwise = @import("bitwise.zig");
|
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.
|
/// Evaluate an AST in programmer mode, producing an exact integer result.
|
||||||
pub fn evalProgrammer(config: ProgrammerConfig, expr: *const Expr) CalcError!Integer {
|
pub fn evalProgrammer(config: Config, expr: *const Expr) CalcError!Integer {
|
||||||
const raw = try evalExpr(config, expr);
|
|
||||||
return .{
|
return .{
|
||||||
.raw = raw & config.bit_width.mask(),
|
.raw = try evalExpr(config, expr),
|
||||||
.bit_width = config.bit_width,
|
.int_type = config.int_type,
|
||||||
.signedness = config.signedness,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively evaluate an expression to a raw u128.
|
/// 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.*) {
|
switch (expr.*) {
|
||||||
.number => |n| {
|
.number => |n| {
|
||||||
if (n.int_value) |int_val| {
|
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.
|
// Float literal in programmer mode: truncate to integer.
|
||||||
// (Number literals are always non-negative; unary minus is a
|
// (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 (!std.math.isFinite(value) or value < 0) return CalcError.DomainError;
|
||||||
if (value >= 340282366920938463463374607431768211456.0) return CalcError.Overflow;
|
if (value >= 340282366920938463463374607431768211456.0) return CalcError.Overflow;
|
||||||
const val: u128 = @intFromFloat(value);
|
const val: u128 = @intFromFloat(value);
|
||||||
return val & config.bit_width.mask();
|
return val & config.int_type.mask();
|
||||||
},
|
},
|
||||||
.string_literal => |text| {
|
.string_literal => |text| {
|
||||||
// Pack ASCII bytes into integer.
|
// Pack ASCII bytes into integer.
|
||||||
// Big-endian packing: first char -> most significant used byte.
|
// 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;
|
if (text.len > max_bytes) return CalcError.Overflow;
|
||||||
var result: u128 = 0;
|
var result: u128 = 0;
|
||||||
for (text) |byte| {
|
for (text) |byte| {
|
||||||
if (byte > 0x7F) return CalcError.InvalidNumber;
|
if (byte > 0x7F) return CalcError.InvalidNumber;
|
||||||
result = (result << 8) | byte;
|
result = (result << 8) | byte;
|
||||||
}
|
}
|
||||||
return result & config.bit_width.mask();
|
return result & config.int_type.mask();
|
||||||
},
|
},
|
||||||
.variable => {
|
.variable => {
|
||||||
return CalcError.UnknownVariable;
|
return CalcError.UnknownVariable;
|
||||||
|
|
@ -67,7 +76,7 @@ fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u128 {
|
||||||
},
|
},
|
||||||
.unary => |u| {
|
.unary => |u| {
|
||||||
const operand = try evalExpr(config, u.operand);
|
const operand = try evalExpr(config, u.operand);
|
||||||
const domain = bitwise.Domain.fromConfig(config);
|
const domain = config.int_type;
|
||||||
return switch (u.op) {
|
return switch (u.op) {
|
||||||
.negate => bitwise.negate(domain, operand),
|
.negate => bitwise.negate(domain, operand),
|
||||||
.bitwise_not => bitwise.not(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
|
/// `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
|
/// 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.
|
/// 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 {
|
fn evalBinaryOp(config: Config, op: BinaryOp, left: u128, right: u128) CalcError!u128 {
|
||||||
const mask = config.bit_width.mask();
|
const mask = config.int_type.mask();
|
||||||
|
|
||||||
const result: u128 = switch (op) {
|
const result: u128 = switch (op) {
|
||||||
.add => (left +% right) & mask,
|
.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`
|
// resolves the operator at comptime, so an operator added to `BinaryOp`
|
||||||
// that `bitwise.fromBinaryOp` does not know is a compile error here.
|
// that `bitwise.fromBinaryOp` does not know is a compile error here.
|
||||||
inline else => |fixed_op| try bitwise.apply(
|
inline else => |fixed_op| try bitwise.apply(
|
||||||
bitwise.Domain.fromConfig(config),
|
config.int_type,
|
||||||
comptime bitwise.fromBinaryOp(fixed_op).?,
|
comptime bitwise.fromBinaryOp(fixed_op).?,
|
||||||
left,
|
left,
|
||||||
right,
|
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.
|
/// 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);
|
var p = Parser.init(allocator, source);
|
||||||
const expr = try p.parse();
|
const expr = try p.parse();
|
||||||
// Same ownership rule as evalStringInfo: the tree is ours to release, and the
|
// 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, .{});
|
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);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
return evalProgrammerString(arena.allocator(), source, config);
|
return evalProgrammerString(arena.allocator(), source, .{ .int_type = int_type });
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: simple number" {
|
test "prog: simple number" {
|
||||||
|
|
@ -177,7 +188,7 @@ test "prog: addition" {
|
||||||
|
|
||||||
test "prog: subtraction wrapping" {
|
test "prog: subtraction wrapping" {
|
||||||
// 5 - 10 in 8-bit unsigned wraps
|
// 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(u128, 251), result.unsignedValue()); // 256 - 5
|
||||||
try testing.expectEqual(@as(i128, -5), result.signedValue());
|
try testing.expectEqual(@as(i128, -5), result.signedValue());
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +199,7 @@ test "prog: multiplication" {
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: multiplication overflow 8-bit" {
|
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
|
// 400 & 0xFF = 144
|
||||||
try testing.expectEqual(@as(u128, 144), result.unsignedValue());
|
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());
|
try testing.expectEqual(@as(u128, 0x0F), a.unsignedValue());
|
||||||
const o = try testProg("0xF0 or 0x0F");
|
const o = try testProg("0xF0 or 0x0F");
|
||||||
try testing.expectEqual(@as(u128, 0xFF), o.unsignedValue());
|
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());
|
try testing.expectEqual(@as(u128, 0xF0), n.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: bitwise NOT 8-bit" {
|
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());
|
try testing.expectEqual(@as(u128, 0xF0), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: bitwise NOT 16-bit" {
|
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());
|
try testing.expectEqual(@as(u128, 0xFF00), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: bitwise NOT 32-bit" {
|
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());
|
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" {
|
test "prog: shift left past the width shifts everything out" {
|
||||||
// The distance used to be clamped to width - 1, so this gave 128.
|
// 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());
|
try testing.expectEqual(@as(u128, 0), result.unsignedValue());
|
||||||
|
|
||||||
// One less than the width still keeps the bit.
|
// 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());
|
try testing.expectEqual(@as(u128, 128), edge.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: logical shift right" {
|
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());
|
try testing.expectEqual(@as(u128, 0x08), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: arithmetic shift right (sign bit preserved)" {
|
test "prog: arithmetic shift right (sign bit preserved)" {
|
||||||
// 0x80 in 8-bit is -128; >> 1 should give 0xC0 (-64)
|
// 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(u128, 0xC0), result.unsignedValue());
|
||||||
try testing.expectEqual(@as(i128, -64), result.signedValue());
|
try testing.expectEqual(@as(i128, -64), result.signedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: arithmetic shift right (positive)" {
|
test "prog: arithmetic shift right (positive)" {
|
||||||
// 0x40 in 8-bit is positive; >> 1 should give 0x20
|
// 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());
|
try testing.expectEqual(@as(u128, 0x20), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: rotate left 8-bit" {
|
test "prog: rotate left 8-bit" {
|
||||||
// 0x81 rol 1 in 8-bit: bit 7 wraps to bit 0 -> 0x03
|
// 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());
|
try testing.expectEqual(@as(u128, 0x03), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: rotate right 8-bit" {
|
test "prog: rotate right 8-bit" {
|
||||||
// 0x81 ror 1 in 8-bit: bit 0 wraps to bit 7 -> 0xC0
|
// 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());
|
try testing.expectEqual(@as(u128, 0xC0), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: negation two's complement" {
|
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(u128, 0xFF), result.unsignedValue());
|
||||||
try testing.expectEqual(@as(i128, -1), result.signedValue());
|
try testing.expectEqual(@as(i128, -1), result.signedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: negation 16-bit" {
|
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());
|
try testing.expectEqual(@as(i128, -42), result.signedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,23 +345,23 @@ test "prog: chained shifts" {
|
||||||
|
|
||||||
test "prog: mask applied to input" {
|
test "prog: mask applied to input" {
|
||||||
// 0x1FF in 8-bit mode should be masked to 0xFF
|
// 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());
|
try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: 32-bit operations" {
|
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());
|
try testing.expectEqual(@as(u128, 0), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: 64-bit max" {
|
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());
|
try testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: negative float input" {
|
test "prog: negative float input" {
|
||||||
// -5.0 as a float in programmer mode should become two's complement
|
// -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());
|
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" {
|
test "prog: ASCII literal overflow 8-bit" {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
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);
|
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
|
// 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,
|
// 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.
|
// 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(u128, 0xFF), result.unsignedValue());
|
||||||
try testing.expectEqual(@as(i128, -1), result.signedValue());
|
try testing.expectEqual(@as(i128, -1), result.signedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: logical shift right past the width shifts everything out" {
|
test "prog: logical shift right past the width shifts everything out" {
|
||||||
// Used to clamp the distance to 7 and give 1.
|
// 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());
|
try testing.expectEqual(@as(u128, 0), result.unsignedValue());
|
||||||
|
|
||||||
// One less than the width still keeps the bottom bit.
|
// 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());
|
try testing.expectEqual(@as(u128, 1), edge.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "prog: arithmetic shift right past the width, positive value" {
|
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.
|
// 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());
|
try testing.expectEqual(@as(u128, 0), result.unsignedValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "no leak: evalProgrammerString releases the parsed tree" {
|
test "no leak: evalProgrammerString releases the parsed tree" {
|
||||||
// testing.allocator rather than an arena, so a retained AST fails the test.
|
// 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" };
|
const good = [_][]const u8{ "0xFF and 0x0F", "1 << 8", "not 0", "0b1010 xor 0b0101", "5 rol 2" };
|
||||||
for (good) |source| {
|
for (good) |source| {
|
||||||
_ = try evalProgrammerString(std.testing.allocator, source, config);
|
_ = 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" {
|
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
|
// `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.
|
// past u128 is illegal behaviour, and only 3.14 was ever tested.
|
||||||
try std.testing.expectError(
|
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" {
|
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.
|
// 10^400 overflows the exact tier's float projection to infinity.
|
||||||
try std.testing.expectError(
|
try std.testing.expectError(
|
||||||
CalcError.DomainError,
|
CalcError.DomainError,
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,16 @@
|
||||||
//! literals, and space/comma/underscore digit separators.
|
//! literals, and space/comma/underscore digit separators.
|
||||||
|
|
||||||
const std = @import("std");
|
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 {
|
pub const TokenKind = enum {
|
||||||
// Literals
|
// 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.
|
//! No allocation, no I/O. Adding a unit means adding a table entry.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const types = @import("types.zig");
|
const CalcError = @import("errors.zig").CalcError;
|
||||||
const CalcError = types.CalcError;
|
|
||||||
const rational_mod = @import("rational.zig");
|
const rational_mod = @import("rational.zig");
|
||||||
const Rational = rational_mod.Rational;
|
const Rational = rational_mod.Rational;
|
||||||
const number_mod = @import("number.zig");
|
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,
|
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.
|
/// Parse CLI args and determine the expression and mode.
|
||||||
/// Returns the joined expression and mode, or an error/help output.
|
/// Returns the joined expression and mode, or an error/help output.
|
||||||
pub const ParsedArgs = union(enum) {
|
pub const ParsedArgs = union(enum) {
|
||||||
expression: struct {
|
expression: struct {
|
||||||
text: []const u8,
|
text: []const u8,
|
||||||
mode: engine.Mode,
|
mode: Mode,
|
||||||
/// Width, signedness and byte order for programmer mode. The CLI accepts
|
/// 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 same settings the TUI has, so a session in one can be reproduced in
|
||||||
/// the other.
|
/// the other.
|
||||||
config: engine.types.ProgrammerConfig = .{},
|
config: engine.programmer.Config = .{},
|
||||||
},
|
},
|
||||||
conversion: struct {
|
conversion: struct {
|
||||||
/// Kept as text so it can be parsed exactly rather than through f64.
|
/// 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 {
|
pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedArgs {
|
||||||
var mode: engine.Mode = .standard;
|
var mode: Mode = .standard;
|
||||||
var config: engine.types.ProgrammerConfig = .{};
|
var config: engine.programmer.Config = .{};
|
||||||
var expr_parts = std.ArrayList([]const u8).empty;
|
var expr_parts = std.ArrayList([]const u8).empty;
|
||||||
defer expr_parts.deinit(allocator);
|
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")) {
|
} else if (std.mem.eql(u8, arg, "--version")) {
|
||||||
return .{ .output = .{ .text = "tally 0.1.0\n", .is_error = false } };
|
return .{ .output = .{ .text = "tally 0.1.0\n", .is_error = false } };
|
||||||
} else if (std.mem.eql(u8, arg, "--signed")) {
|
} else if (std.mem.eql(u8, arg, "--signed")) {
|
||||||
config.signedness = .signed;
|
config.int_type.signedness = .signed;
|
||||||
mode = .programmer;
|
mode = .programmer;
|
||||||
} else if (std.mem.eql(u8, arg, "--unsigned")) {
|
} else if (std.mem.eql(u8, arg, "--unsigned")) {
|
||||||
config.signedness = .unsigned;
|
config.int_type.signedness = .unsigned;
|
||||||
mode = .programmer;
|
mode = .programmer;
|
||||||
} else {
|
} else {
|
||||||
// Flags that take a value, accepted as either `--bits 8` or `--bits=8`.
|
// 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 => {},
|
.absent => {},
|
||||||
.missing => return .{ .output = .{ .text = bits_usage, .is_error = true } },
|
.missing => return .{ .output = .{ .text = bits_usage, .is_error = true } },
|
||||||
.value => |text| {
|
.value => |text| {
|
||||||
config.bit_width = parseBitWidth(text) orelse
|
config.int_type.width = parseBitWidth(text) orelse
|
||||||
return .{ .output = .{ .text = bits_usage, .is_error = true } };
|
return .{ .output = .{ .text = bits_usage, .is_error = true } };
|
||||||
// The width only means something in programmer mode: standard
|
// The width only means something in programmer mode: standard
|
||||||
// mode is fixed at 64-bit signed. Asking for a width is asking
|
// 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;
|
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
|
// Driven by the enum, so a width added to `BitWidth` is accepted here without
|
||||||
// a second list to update.
|
// 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}))) {
|
if (std.mem.eql(u8, text, comptime std.fmt.comptimePrint("{d}", .{field.value}))) {
|
||||||
return @enumFromInt(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".
|
/// The accepted widths, as they appear in messages: "8, 16, 32, 64, 128".
|
||||||
const bit_width_list = blk: {
|
const bit_width_list = blk: {
|
||||||
var list: []const u8 = "";
|
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});
|
list = list ++ (if (i == 0) "" else ", ") ++ std.fmt.comptimePrint("{d}", .{field.value});
|
||||||
}
|
}
|
||||||
break :blk list;
|
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, "big") or std.ascii.eqlIgnoreCase(text, "be")) return .big;
|
||||||
if (std.ascii.eqlIgnoreCase(text, "little") or std.ascii.eqlIgnoreCase(text, "le")) return .little;
|
if (std.ascii.eqlIgnoreCase(text, "little") or std.ascii.eqlIgnoreCase(text, "le")) return .little;
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -404,7 +411,7 @@ fn formatConversionUnits(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate an expression and format the result as a string.
|
/// 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);
|
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(
|
pub fn evaluateWith(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
expression: []const u8,
|
expression: []const u8,
|
||||||
mode: engine.Mode,
|
mode: Mode,
|
||||||
config: engine.types.ProgrammerConfig,
|
config: engine.programmer.Config,
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
) CliResult {
|
) CliResult {
|
||||||
if (mode == .programmer) {
|
if (mode == .programmer) {
|
||||||
|
|
@ -424,7 +431,7 @@ pub fn evaluateWith(
|
||||||
return formatProgrammerResult(buf, result, config);
|
return formatProgrammerResult(buf, result, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
var env = engine.Environment.init(allocator, .standard);
|
var env = engine.Environment.init(allocator);
|
||||||
defer env.deinit();
|
defer env.deinit();
|
||||||
|
|
||||||
// A standalone "to" keyword means this is a unit conversion, e.g.
|
// 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.
|
/// The decimal display is already computed; append hex/oct/bin.
|
||||||
fn formatStandardMultiBase(buf: []u8, dec_display: []const u8, value: f64) CliResult {
|
fn formatStandardMultiBase(buf: []u8, dec_display: []const u8, value: f64) CliResult {
|
||||||
const int_val: u128 = @intFromFloat(value);
|
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 hex_buf: [256]u8 = undefined;
|
||||||
var oct_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 };
|
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 value = result.unsignedValue();
|
||||||
const signed = result.signedValue();
|
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 oct_buf: [256]u8 = undefined;
|
||||||
var bin_buf: [512]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 dec = engine.formatter.formatDecimalUnsigned(&dec_buf, value);
|
||||||
const sdec = engine.formatter.formatDecimalSigned(&sdec_buf, signed);
|
const sdec = engine.formatter.formatDecimalSigned(&sdec_buf, signed);
|
||||||
const oct = engine.formatter.formatOctal(&oct_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.bit_width);
|
const bin = engine.formatter.formatBinary(&bin_buf, value, config.int_type.width);
|
||||||
|
|
||||||
const output = std.fmt.bufPrint(buf,
|
const output = std.fmt.bufPrint(buf,
|
||||||
\\ dec(signed): {s}
|
\\ 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.
|
/// 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
|
/// 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
|
/// 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
|
/// 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".
|
/// silently reads "evaluation error".
|
||||||
fn decoratedError(err: engine.CalcError) []const u8 {
|
fn decoratedError(err: engine.CalcError) []const u8 {
|
||||||
return switch (err) {
|
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) {
|
switch (parsed) {
|
||||||
.expression => |e| {
|
.expression => |e| {
|
||||||
try testing.expectEqualStrings("2+2", e.text);
|
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);
|
testing.allocator.free(e.text);
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
|
|
@ -910,7 +917,7 @@ test "parseArgs: programmer flag" {
|
||||||
const parsed = parseArgs(testing.allocator, &.{ "-p", "0xFF" });
|
const parsed = parseArgs(testing.allocator, &.{ "-p", "0xFF" });
|
||||||
switch (parsed) {
|
switch (parsed) {
|
||||||
.expression => |e| {
|
.expression => |e| {
|
||||||
try testing.expectEqual(engine.Mode.programmer, e.mode);
|
try testing.expectEqual(Mode.programmer, e.mode);
|
||||||
try testing.expectEqualStrings("0xFF", e.text);
|
try testing.expectEqualStrings("0xFF", e.text);
|
||||||
testing.allocator.free(e.text);
|
testing.allocator.free(e.text);
|
||||||
},
|
},
|
||||||
|
|
@ -922,7 +929,7 @@ test "parseArgs: --programmer long flag" {
|
||||||
const parsed = parseArgs(testing.allocator, &.{ "--programmer", "0xF0", "|", "0x0F" });
|
const parsed = parseArgs(testing.allocator, &.{ "--programmer", "0xF0", "|", "0x0F" });
|
||||||
switch (parsed) {
|
switch (parsed) {
|
||||||
.expression => |e| {
|
.expression => |e| {
|
||||||
try testing.expectEqual(engine.Mode.programmer, e.mode);
|
try testing.expectEqual(Mode.programmer, e.mode);
|
||||||
try testing.expectEqualStrings("0xF0 | 0x0F", e.text);
|
try testing.expectEqualStrings("0xF0 | 0x0F", e.text);
|
||||||
testing.allocator.free(e.text);
|
testing.allocator.free(e.text);
|
||||||
},
|
},
|
||||||
|
|
@ -937,16 +944,16 @@ test "parseArgs: --bits sets the width, in either spelling, and implies -p" {
|
||||||
}) |args| {
|
}) |args| {
|
||||||
const e = parseArgs(testing.allocator, args).expression;
|
const e = parseArgs(testing.allocator, args).expression;
|
||||||
defer testing.allocator.free(e.text);
|
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
|
// Asking for a width is asking for programmer mode: standard mode is fixed
|
||||||
// at 64-bit signed, so the flag would mean nothing there.
|
// 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);
|
try testing.expectEqualStrings("0xFF", e.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseArgs: every documented bit width is accepted" {
|
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 = "8", .expected = .bits8 },
|
||||||
.{ .text = "16", .expected = .bits16 },
|
.{ .text = "16", .expected = .bits16 },
|
||||||
.{ .text = "32", .expected = .bits32 },
|
.{ .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" {
|
test "parseArgs: --signed and --unsigned set the signedness and imply -p" {
|
||||||
const signed = parseArgs(testing.allocator, &.{ "--signed", "0xFF" }).expression;
|
const signed = parseArgs(testing.allocator, &.{ "--signed", "0xFF" }).expression;
|
||||||
defer testing.allocator.free(signed.text);
|
defer testing.allocator.free(signed.text);
|
||||||
try testing.expectEqual(engine.types.Signedness.signed, signed.config.signedness);
|
try testing.expectEqual(engine.Integer.Signedness.signed, signed.config.int_type.signedness);
|
||||||
try testing.expectEqual(engine.Mode.programmer, signed.mode);
|
try testing.expectEqual(Mode.programmer, signed.mode);
|
||||||
|
|
||||||
const unsigned = parseArgs(testing.allocator, &.{ "--unsigned", "0xFF" }).expression;
|
const unsigned = parseArgs(testing.allocator, &.{ "--unsigned", "0xFF" }).expression;
|
||||||
defer testing.allocator.free(unsigned.text);
|
defer testing.allocator.free(unsigned.text);
|
||||||
try testing.expectEqual(engine.types.Signedness.unsigned, unsigned.config.signedness);
|
try testing.expectEqual(engine.Integer.Signedness.unsigned, unsigned.config.int_type.signedness);
|
||||||
try testing.expectEqual(engine.Mode.programmer, unsigned.mode);
|
try testing.expectEqual(Mode.programmer, unsigned.mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseArgs: --endian sets the byte order and accepts both spellings" {
|
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| {
|
}) |args| {
|
||||||
const e = parseArgs(testing.allocator, args).expression;
|
const e = parseArgs(testing.allocator, args).expression;
|
||||||
defer testing.allocator.free(e.text);
|
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(std.builtin.Endian.big, parseEndian("big").?);
|
||||||
try testing.expectEqual(engine.types.Endianness.big, parseEndian("BE").?);
|
try testing.expectEqual(std.builtin.Endian.big, parseEndian("BE").?);
|
||||||
try testing.expect(parseEndian("middle") == null);
|
try testing.expect(parseEndian("middle") == null);
|
||||||
try testing.expect(parseEndian("") == 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" {
|
test "parseArgs: the flags combine, and order does not matter" {
|
||||||
const e = parseArgs(testing.allocator, &.{ "--unsigned", "--bits", "16", "--endian", "little", "0xFF", "+", "1" }).expression;
|
const e = parseArgs(testing.allocator, &.{ "--unsigned", "--bits", "16", "--endian", "little", "0xFF", "+", "1" }).expression;
|
||||||
defer testing.allocator.free(e.text);
|
defer testing.allocator.free(e.text);
|
||||||
try testing.expectEqual(engine.types.BitWidth.bits16, e.config.bit_width);
|
try testing.expectEqual(engine.Integer.BitWidth.bits16, e.config.int_type.width);
|
||||||
try testing.expectEqual(engine.types.Signedness.unsigned, e.config.signedness);
|
try testing.expectEqual(engine.Integer.Signedness.unsigned, e.config.int_type.signedness);
|
||||||
try testing.expectEqual(engine.types.Endianness.little, e.config.display_endian);
|
try testing.expectEqual(std.builtin.Endian.little, e.config.display_endian);
|
||||||
try testing.expectEqualStrings("0xFF + 1", e.text);
|
try testing.expectEqualStrings("0xFF + 1", e.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1043,31 +1050,29 @@ test "the programmer config reaches the result" {
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
// 8-bit: 0xFF + 1 wraps to 0.
|
// 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(!narrow.is_error);
|
||||||
try testing.expect(std.mem.indexOf(u8, narrow.output, "dec(unsigned): 0\n") != null);
|
try testing.expect(std.mem.indexOf(u8, narrow.output, "dec(unsigned): 0\n") != null);
|
||||||
|
|
||||||
// Signedness decides whether >> extends the sign.
|
// Signedness decides whether >> extends the sign.
|
||||||
const signed = evaluateWith(alloc, "0xFF >> 1", .programmer, .{
|
const signed = evaluateWith(alloc, "0xFF >> 1", .programmer, .{
|
||||||
.bit_width = .bits8,
|
.int_type = .{ .width = .bits8, .signedness = .signed },
|
||||||
.signedness = .signed,
|
|
||||||
}, &buf);
|
}, &buf);
|
||||||
try testing.expect(std.mem.indexOf(u8, signed.output, "dec(signed): -1") != null);
|
try testing.expect(std.mem.indexOf(u8, signed.output, "dec(signed): -1") != null);
|
||||||
|
|
||||||
const unsigned = evaluateWith(alloc, "0xFF >> 1", .programmer, .{
|
const unsigned = evaluateWith(alloc, "0xFF >> 1", .programmer, .{
|
||||||
.bit_width = .bits8,
|
.int_type = .{ .width = .bits8, .signedness = .unsigned },
|
||||||
.signedness = .unsigned,
|
|
||||||
}, &buf);
|
}, &buf);
|
||||||
try testing.expect(std.mem.indexOf(u8, unsigned.output, "dec(unsigned): 127") != null);
|
try testing.expect(std.mem.indexOf(u8, unsigned.output, "dec(unsigned): 127") != null);
|
||||||
|
|
||||||
// Byte order reverses the hex row and nothing else.
|
// Byte order reverses the hex row and nothing else.
|
||||||
const little = evaluateWith(alloc, "0xDEAD", .programmer, .{
|
const little = evaluateWith(alloc, "0xDEAD", .programmer, .{
|
||||||
.bit_width = .bits16,
|
.int_type = .{ .width = .bits16 },
|
||||||
.display_endian = .little,
|
.display_endian = .little,
|
||||||
}, &buf);
|
}, &buf);
|
||||||
try testing.expect(std.mem.indexOf(u8, little.output, "hex: AD DE") != null);
|
try testing.expect(std.mem.indexOf(u8, little.output, "hex: AD DE") != null);
|
||||||
const big = evaluateWith(alloc, "0xDEAD", .programmer, .{
|
const big = evaluateWith(alloc, "0xDEAD", .programmer, .{
|
||||||
.bit_width = .bits16,
|
.int_type = .{ .width = .bits16 },
|
||||||
.display_endian = .big,
|
.display_endian = .big,
|
||||||
}, &buf);
|
}, &buf);
|
||||||
try testing.expect(std.mem.indexOf(u8, big.output, "hex: DE AD") != null);
|
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,
|
mode: Mode,
|
||||||
// Programmer mode state
|
// Programmer mode state
|
||||||
prog_value: u128,
|
prog_value: u128,
|
||||||
prog_config: engine.types.ProgrammerConfig,
|
prog_config: engine.programmer.Config,
|
||||||
bit_cursor: u7,
|
bit_cursor: u7,
|
||||||
prog_field: ProgField,
|
prog_field: ProgField,
|
||||||
value_zone_active: bool, // true = cursor in value display, false = in input
|
value_zone_active: bool, // true = cursor in value display, false = in input
|
||||||
|
|
@ -219,7 +219,7 @@ pub const App = struct {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.io = io,
|
.io = io,
|
||||||
.env = engine.evaluator.Environment.init(allocator, .standard),
|
.env = engine.evaluator.Environment.init(allocator),
|
||||||
.input = text_field,
|
.input = text_field,
|
||||||
.history = .empty,
|
.history = .empty,
|
||||||
.show_help = false,
|
.show_help = false,
|
||||||
|
|
@ -373,7 +373,7 @@ pub const App = struct {
|
||||||
self.value_zone_active = true;
|
self.value_zone_active = true;
|
||||||
self.prog_field = target.field;
|
self.prog_field = target.field;
|
||||||
if (target.bit) |bit| {
|
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 {
|
} else {
|
||||||
self.alignCursorToField();
|
self.alignCursorToField();
|
||||||
}
|
}
|
||||||
|
|
@ -381,10 +381,10 @@ pub const App = struct {
|
||||||
.toggle_bit => |bit| {
|
.toggle_bit => |bit| {
|
||||||
self.value_zone_active = true;
|
self.value_zone_active = true;
|
||||||
self.prog_field = if (self.prog_field == .bin) .bin else .bits;
|
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.bit_cursor = bit;
|
||||||
self.prog_value ^= @as(u128, 1) << 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,
|
.focus_input => self.value_zone_active = false,
|
||||||
|
|
@ -429,10 +429,9 @@ pub const App = struct {
|
||||||
/// Switch modes, carrying the last answer into programmer mode.
|
/// Switch modes, carrying the last answer into programmer mode.
|
||||||
fn setMode(self: *App, new_mode: Mode) void {
|
fn setMode(self: *App, new_mode: Mode) void {
|
||||||
self.mode = new_mode;
|
self.mode = new_mode;
|
||||||
self.env.mode = switch (new_mode) {
|
// The environment has no mode to set: which evaluator runs is decided at
|
||||||
.programmer => .programmer,
|
// the call, not by state. This used to write `self.env.mode`, a field the
|
||||||
else => .standard,
|
// engine never read.
|
||||||
};
|
|
||||||
if (new_mode == .programmer) self.loadAnsIntoProgrammer();
|
if (new_mode == .programmer) self.loadAnsIntoProgrammer();
|
||||||
self.value_zone_active = false;
|
self.value_zone_active = false;
|
||||||
}
|
}
|
||||||
|
|
@ -450,15 +449,15 @@ pub const App = struct {
|
||||||
const signed: i128 = @intFromFloat(ans);
|
const signed: i128 = @intFromFloat(ans);
|
||||||
self.prog_value = @bitCast(signed);
|
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 {
|
} else {
|
||||||
self.float_format = .f64;
|
self.float_format = .f64;
|
||||||
self.float_view_active = true;
|
self.float_view_active = true;
|
||||||
self.syncFloatWidth();
|
self.syncFloatWidth();
|
||||||
self.prog_value = @as(u64, @bitCast(ans));
|
self.prog_value = @as(u64, @bitCast(ans));
|
||||||
self.prog_field = .bits;
|
self.prog_field = .bits;
|
||||||
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
|
if (self.bit_cursor >= self.prog_config.int_type.width.bits()) {
|
||||||
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
|
self.bit_cursor = @intCast(self.prog_config.int_type.width.bits() - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -652,8 +651,8 @@ pub const App = struct {
|
||||||
self.syncFloatWidth();
|
self.syncFloatWidth();
|
||||||
// Keep the bit grid focused so arrows/space edit bits directly.
|
// Keep the bit grid focused so arrows/space edit bits directly.
|
||||||
self.prog_field = .bits;
|
self.prog_field = .bits;
|
||||||
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
|
if (self.bit_cursor >= self.prog_config.int_type.width.bits()) {
|
||||||
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
|
self.bit_cursor = @intCast(self.prog_config.int_type.width.bits() - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.redraw = true;
|
ctx.redraw = true;
|
||||||
|
|
@ -790,7 +789,7 @@ pub const App = struct {
|
||||||
// Up/Down: move between fields
|
// Up/Down: move between fields
|
||||||
if (key.matches(vaxis.Key.up, .{})) {
|
if (key.matches(vaxis.Key.up, .{})) {
|
||||||
if (self.prog_field == .bits) {
|
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;
|
const bits_per_row: u8 = if (width > 32) 32 else width;
|
||||||
if (@as(u8, self.bit_cursor) + bits_per_row < width) {
|
if (@as(u8, self.bit_cursor) + bits_per_row < width) {
|
||||||
self.bit_cursor += @intCast(bits_per_row);
|
self.bit_cursor += @intCast(bits_per_row);
|
||||||
|
|
@ -803,7 +802,7 @@ pub const App = struct {
|
||||||
}
|
}
|
||||||
if (key.matches(vaxis.Key.down, .{})) {
|
if (key.matches(vaxis.Key.down, .{})) {
|
||||||
if (self.prog_field == .bits) {
|
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;
|
const bits_per_row: u8 = if (width > 32) 32 else width;
|
||||||
if (self.bit_cursor >= bits_per_row) {
|
if (self.bit_cursor >= bits_per_row) {
|
||||||
self.bit_cursor -= @intCast(bits_per_row);
|
self.bit_cursor -= @intCast(bits_per_row);
|
||||||
|
|
@ -819,7 +818,7 @@ pub const App = struct {
|
||||||
if (key.matches(vaxis.Key.left, .{})) {
|
if (key.matches(vaxis.Key.left, .{})) {
|
||||||
const step = self.fieldBitStep();
|
const step = self.fieldBitStep();
|
||||||
if (step > 0) {
|
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) {
|
if (@as(u16, self.bit_cursor) + step < width) {
|
||||||
self.bit_cursor += @intCast(step);
|
self.bit_cursor += @intCast(step);
|
||||||
}
|
}
|
||||||
|
|
@ -840,7 +839,7 @@ pub const App = struct {
|
||||||
if (key.matches(' ', .{})) {
|
if (key.matches(' ', .{})) {
|
||||||
if (self.prog_field == .bits) {
|
if (self.prog_field == .bits) {
|
||||||
self.prog_value ^= @as(u128, 1) << self.bit_cursor;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -868,7 +867,7 @@ pub const App = struct {
|
||||||
const shift: u7 = self.bit_cursor & 0x7C; // round down to nibble boundary
|
const shift: u7 = self.bit_cursor & 0x7C; // round down to nibble boundary
|
||||||
const mask = ~(@as(u128, 0xF) << shift);
|
const mask = ~(@as(u128, 0xF) << shift);
|
||||||
self.prog_value = (self.prog_value & mask) | (@as(u128, n) << 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)
|
// Move cursor right (toward LSB)
|
||||||
if (shift >= 4) self.bit_cursor -= 4;
|
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 shift: u7 = (self.bit_cursor / 3) * 3; // round down to octal boundary
|
||||||
const mask = ~(@as(u128, 0x7) << shift);
|
const mask = ~(@as(u128, 0x7) << shift);
|
||||||
self.prog_value = (self.prog_value & mask) | (@as(u128, digit) << 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;
|
if (shift >= 3) self.bit_cursor -= 3;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -890,7 +889,7 @@ pub const App = struct {
|
||||||
if (self.bit_cursor > 0) self.bit_cursor -= 1;
|
if (self.bit_cursor > 0) self.bit_cursor -= 1;
|
||||||
} else if (char == '1') {
|
} else if (char == '1') {
|
||||||
self.prog_value |= @as(u128, 1) << self.bit_cursor;
|
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;
|
if (self.bit_cursor > 0) self.bit_cursor -= 1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -898,7 +897,7 @@ pub const App = struct {
|
||||||
if (char >= '0' and char <= '9') {
|
if (char >= '0' and char <= '9') {
|
||||||
// Operate on the in-width portion so hidden upper bits do
|
// Operate on the in-width portion so hidden upper bits do
|
||||||
// not corrupt the arithmetic; the edit commits to width.
|
// 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;
|
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
|
/// upper bits. The display masks to width and shows a warning while the
|
||||||
/// value does not fit. Explicit value edits still commit to width.
|
/// value does not fit. Explicit value edits still commit to width.
|
||||||
fn cycleBitWidth(self: *App) void {
|
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,
|
.bits8 => .bits16,
|
||||||
.bits16 => .bits32,
|
.bits16 => .bits32,
|
||||||
.bits32 => .bits64,
|
.bits32 => .bits64,
|
||||||
.bits64 => .bits128,
|
.bits64 => .bits128,
|
||||||
.bits128 => .bits8,
|
.bits128 => .bits8,
|
||||||
};
|
};
|
||||||
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
|
if (self.bit_cursor >= self.prog_config.int_type.width.bits()) {
|
||||||
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
|
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 {
|
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,
|
.signed => .unsigned,
|
||||||
.unsigned => .signed,
|
.unsigned => .signed,
|
||||||
};
|
};
|
||||||
|
|
@ -949,12 +948,12 @@ pub const App = struct {
|
||||||
|
|
||||||
/// Snap the bit width to match the active float format (f32 -> 32, f64 -> 64).
|
/// Snap the bit width to match the active float format (f32 -> 32, f64 -> 64).
|
||||||
fn syncFloatWidth(self: *App) void {
|
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,
|
.f32 => .bits32,
|
||||||
.f64 => .bits64,
|
.f64 => .bits64,
|
||||||
};
|
};
|
||||||
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
|
if (self.bit_cursor >= self.prog_config.int_type.width.bits()) {
|
||||||
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
|
self.bit_cursor = @intCast(self.prog_config.int_type.width.bits() - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1046,7 +1045,7 @@ pub const App = struct {
|
||||||
as_float < 340282366920938463463374607431768211456.0)
|
as_float < 340282366920938463463374607431768211456.0)
|
||||||
{
|
{
|
||||||
const int_val: u128 = @intFromFloat(as_float);
|
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 hex_buf: [256]u8 = undefined;
|
||||||
var oct_buf: [256]u8 = undefined;
|
var oct_buf: [256]u8 = undefined;
|
||||||
var bin_buf: [512]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.
|
/// 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
|
/// comptime. This switch used to be a second full copy of the CLI's, and had fallen
|
||||||
/// behind: `InsufficientParameters`, `ConvergenceFailure` and `InvalidExpression`
|
/// behind: `InsufficientParameters`, `ConvergenceFailure` and `InvalidExpression`
|
||||||
/// all came out as "evaluation error".
|
/// all came out as "evaluation error".
|
||||||
fn errorStr(err: engine.CalcError) []const u8 {
|
fn errorStr(err: engine.CalcError) []const u8 {
|
||||||
return switch (err) {
|
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.
|
// Programmer mode's bit-width cycle still works.
|
||||||
app.setMode(.programmer);
|
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 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" {
|
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();
|
defer app.deinit();
|
||||||
app.setMode(.programmer);
|
app.setMode(.programmer);
|
||||||
app.prog_value = 0xDEADBEEF;
|
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);
|
const rows = try renderApp(arena, &app, 100, 30);
|
||||||
try testing.expect(test_render.contains(rows, "value exceeds 8 bits"));
|
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;
|
app.prog_value = 0xFEDCBA9876543210;
|
||||||
|
|
||||||
for ([_]engine.BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |width| {
|
for ([_]engine.BitWidth{ .bits8, .bits16, .bits32, .bits64, .bits128 }) |width| {
|
||||||
app.prog_config.bit_width = width;
|
app.prog_config.int_type.width = width;
|
||||||
for ([_]engine.types.Endianness{ .little, .big }) |endian| {
|
for ([_]std.builtin.Endian{ .little, .big }) |endian| {
|
||||||
app.prog_config.display_endian = endian;
|
app.prog_config.display_endian = endian;
|
||||||
for ([_]engine.types.Signedness{ .signed, .unsigned }) |signedness| {
|
for ([_]engine.Integer.Signedness{ .signed, .unsigned }) |signedness| {
|
||||||
app.prog_config.signedness = signedness;
|
app.prog_config.int_type.signedness = signedness;
|
||||||
const rows = try renderApp(arena, &app, 100, 40);
|
const rows = try renderApp(arena, &app, 100, 40);
|
||||||
try testing.expect(test_render.furniture(rows).intact());
|
try testing.expect(test_render.furniture(rows).intact());
|
||||||
var buf: [24]u8 = undefined;
|
var buf: [24]u8 = undefined;
|
||||||
|
|
@ -2066,7 +2065,7 @@ test "render: the float view decodes a known bit pattern" {
|
||||||
app.setMode(.programmer);
|
app.setMode(.programmer);
|
||||||
app.float_view_active = true;
|
app.float_view_active = true;
|
||||||
app.float_format = .f32;
|
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)));
|
app.prog_value = @as(u32, @bitCast(@as(f32, 1.0)));
|
||||||
|
|
||||||
const rows = try renderApp(arena, &app, 100, 30);
|
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| {
|
for (cases) |case| {
|
||||||
app.float_format = case.format;
|
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;
|
app.prog_value = case.bits;
|
||||||
const rows = try renderApp(arena, &app, 100, 34);
|
const rows = try renderApp(arena, &app, 100, 34);
|
||||||
try testing.expect(test_render.furniture(rows).intact());
|
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();
|
var app = testApp();
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
app.setMode(.programmer);
|
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
|
// 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.
|
// 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 app.applyAction(&ctx, .{ .prog_field = .{ .field = .oct, .bit = null } });
|
||||||
try testing.expectEqual(App.ProgField.oct, app.prog_field);
|
try testing.expectEqual(App.ProgField.oct, app.prog_field);
|
||||||
// An out-of-width bit is ignored rather than moving the cursor off the value.
|
// 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 app.applyAction(&ctx, .{ .prog_field = .{ .field = .bits, .bit = 100 } });
|
||||||
try testing.expect(app.bit_cursor < 8);
|
try testing.expect(app.bit_cursor < 8);
|
||||||
|
|
||||||
// Toggling bits.
|
// Toggling bits.
|
||||||
app.prog_config.bit_width = .bits32;
|
app.prog_config.int_type.width = .bits32;
|
||||||
app.prog_value = 0;
|
app.prog_value = 0;
|
||||||
try app.applyAction(&ctx, .{ .toggle_bit = 3 });
|
try app.applyAction(&ctx, .{ .toggle_bit = 3 });
|
||||||
try testing.expectEqual(@as(u128, 8), app.prog_value);
|
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);
|
try testing.expectEqual(@as(u128, 0), app.prog_value);
|
||||||
|
|
||||||
// Settings.
|
// 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 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;
|
const endian_before = app.prog_config.display_endian;
|
||||||
try app.applyAction(&ctx, .toggle_endian);
|
try app.applyAction(&ctx, .toggle_endian);
|
||||||
try testing.expect(app.prog_config.display_endian != endian_before);
|
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 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.
|
// Float overlay and its format toggle.
|
||||||
try app.applyAction(&ctx, .toggle_float);
|
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.setMode(.programmer);
|
||||||
app.value_zone_active = true;
|
app.value_zone_active = true;
|
||||||
app.bit_cursor = 100;
|
app.bit_cursor = 100;
|
||||||
app.prog_config.bit_width = .bits8;
|
app.prog_config.int_type.width = .bits8;
|
||||||
|
|
||||||
var seen: usize = 0;
|
var seen: usize = 0;
|
||||||
while (seen < 6) : (seen += 1) {
|
while (seen < 6) : (seen += 1) {
|
||||||
try press(&app, &ctx, .{ .codepoint = 'w', .mods = .{ .ctrl = true } });
|
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.
|
// 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" {
|
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);
|
defer ctx.cmds.deinit(testing.allocator);
|
||||||
app.setMode(.programmer);
|
app.setMode(.programmer);
|
||||||
app.value_zone_active = true;
|
app.value_zone_active = true;
|
||||||
app.prog_config.bit_width = .bits32;
|
app.prog_config.int_type.width = .bits32;
|
||||||
|
|
||||||
// Hex nibble entry.
|
// Hex nibble entry.
|
||||||
app.prog_field = .hex;
|
app.prog_field = .hex;
|
||||||
|
|
@ -2669,7 +2668,7 @@ test "programmer mode: arrows and space navigate the bit grid" {
|
||||||
defer ctx.cmds.deinit(testing.allocator);
|
defer ctx.cmds.deinit(testing.allocator);
|
||||||
app.setMode(.programmer);
|
app.setMode(.programmer);
|
||||||
app.value_zone_active = true;
|
app.value_zone_active = true;
|
||||||
app.prog_config.bit_width = .bits64;
|
app.prog_config.int_type.width = .bits64;
|
||||||
app.prog_field = .bits;
|
app.prog_field = .bits;
|
||||||
app.bit_cursor = 0;
|
app.bit_cursor = 0;
|
||||||
|
|
||||||
|
|
@ -2737,7 +2736,7 @@ test "float view: typing a decimal stores the nearest bit pattern" {
|
||||||
app.setMode(.programmer);
|
app.setMode(.programmer);
|
||||||
app.float_view_active = true;
|
app.float_view_active = true;
|
||||||
app.float_format = .f64;
|
app.float_format = .f64;
|
||||||
app.prog_config.bit_width = .bits64;
|
app.prog_config.int_type.width = .bits64;
|
||||||
|
|
||||||
try app.input.insertSliceAtCursor("3.14");
|
try app.input.insertSliceAtCursor("3.14");
|
||||||
try press(&app, &ctx, .{ .codepoint = vaxis.Key.enter });
|
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.
|
// In the float overlay Ctrl-W swaps format instead of cycling width.
|
||||||
try press(&app, &ctx, .{ .codepoint = 'w', .mods = .{ .ctrl = true } });
|
try press(&app, &ctx, .{ .codepoint = 'w', .mods = .{ .ctrl = true } });
|
||||||
try testing.expectEqual(engine.FloatFormat.f32, app.float_format);
|
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.
|
// Ctrl-F leaves the overlay.
|
||||||
try press(&app, &ctx, .{ .codepoint = 'f', .mods = .{ .ctrl = true } });
|
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| {
|
for (fields) |field| {
|
||||||
app.prog_field = field;
|
app.prog_field = field;
|
||||||
for ([_]engine.BitWidth{ .bits8, .bits64, .bits128 }) |width| {
|
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));
|
app.bit_cursor = @intCast(@min(5, width.bits() - 1));
|
||||||
const rows = try renderApp(arena, &app, 110, 40);
|
const rows = try renderApp(arena, &app, 110, 40);
|
||||||
try testing.expect(test_render.furniture(rows).intact());
|
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.
|
/// Error text for this view.
|
||||||
///
|
///
|
||||||
/// Only the cases where a form knows more than the engine does are overridden; the
|
/// 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
|
/// 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.
|
/// is always one of a few bad entries, but "division by zero" needs no improving.
|
||||||
pub fn errorText(err: engine.CalcError) []const u8 {
|
pub fn errorText(err: engine.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.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.ConvergenceFailure => "no rate solves these cash flows",
|
||||||
engine.CalcError.InsufficientParameters => "these values do not determine an answer",
|
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;
|
_ = width;
|
||||||
const format = app.float_format;
|
const format = app.float_format;
|
||||||
const total = format.totalBits();
|
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 bits: u64 = @truncate(app.prog_value & bw.mask());
|
||||||
const info = fi.decompose(format, bits);
|
const info = fi.decompose(format, bits);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const tui = @import("../tui.zig");
|
||||||
const C = draw.C;
|
const C = draw.C;
|
||||||
|
|
||||||
pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, height: u16) void {
|
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 val = app.prog_value & bw.mask();
|
||||||
const focused = app.prog_field;
|
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;
|
var config_buf: [80]u8 = undefined;
|
||||||
const config_str = std.fmt.bufPrint(&config_buf, "Bits: {d} Signed: {s} Endian: {s}", .{
|
const config_str = std.fmt.bufPrint(&config_buf, "Bits: {d} Signed: {s} Endian: {s}", .{
|
||||||
bw.bits(),
|
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",
|
if (app.prog_config.display_endian == .little) "LE" else "BE",
|
||||||
}) catch "Bits: ??";
|
}) catch "Bits: ??";
|
||||||
draw.writeStr(surface, 2, 2, config_str, .{ .fg = C.muted });
|
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);
|
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)
|
// DEC(s)
|
||||||
var sdec_buf: [256]u8 = undefined;
|
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 width_bits = bw.bits();
|
||||||
const bits_per_row: u8 = if (width_bits > 32) 32 else width_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;
|
const num_rows: u16 = (@as(u16, width_bits) + bits_per_row - 1) / bits_per_row;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue