human review: Integer.zig

This commit is contained in:
Emil Lerch 2026-07-30 16:56:31 -07:00
parent 1209a0d80b
commit 880c2e657f
Signed by: lobo
GPG key ID: A7B62D657EF764F8
8 changed files with 101 additions and 71 deletions

View file

@ -25,24 +25,14 @@ width: BitWidth = .bits64,
/// Whether the top bit is a sign.
signedness: Signedness = .signed,
/// All bits set within the width.
pub fn mask(self: Integer) u128 {
return self.width.mask();
}
/// The width in bits.
pub fn bits(self: Integer) u8 {
return self.width.bits();
}
/// The top bit's position, whether or not this value treats it as a sign.
pub fn topBit(self: Integer) u128 {
return @as(u128, 1) << @intCast(self.bits() - 1);
return @as(u128, 1) << @intCast(self.width.bits() - 1);
}
/// The pattern, truncated to the width.
pub fn masked(self: Integer) u128 {
return self.raw & self.mask();
return self.raw & self.width.mask();
}
/// True when the pattern denotes a negative number. An unsigned value has no
@ -55,7 +45,7 @@ pub fn isNegative(self: Integer) bool {
/// Interpret as a signed value, sign-extended from the width.
pub fn signedValue(self: Integer) i128 {
const m = self.masked();
if (self.isNegative()) return @bitCast(m | ~self.mask());
if (self.isNegative()) return @bitCast(m | ~self.width.mask());
return @intCast(m);
}
@ -67,7 +57,7 @@ pub fn unsignedValue(self: Integer) u128 {
/// The same width and signedness, a different pattern. How operations return their
/// result without restating the type.
pub fn withRaw(self: Integer, raw: u128) Integer {
return .{ .raw = raw & self.mask(), .width = self.width, .signedness = self.signedness };
return .{ .raw = raw & self.width.mask(), .width = self.width, .signedness = self.signedness };
}
/// True when two values are the same kind of integer, so an operation between them
@ -83,12 +73,25 @@ pub fn sameTypeAs(self: Integer, other: Integer) bool {
// frontends held an `Integer` and then handed the pieces over, so nothing stopped a
// pattern being rendered at a width it did not come from.
//
// `as` returns a view, which is the value plus its formatting options and a `format`
// method, so a caller prints it: `w.print("hex: {f}\n", .{int.as(.hex, .{})})`. A
// caller that wants bytes calls `render` with a buffer, which is a `Writer.fixed`
// underneath. Writing into a buffer too small for the result is
// `error.WriteFailed` rather than a walk off the end, which is what the previous
// buffer-indexing version did.
// `{f}` on the value gives plain decimal. `fmt` returns a `Format`, which is the
// value plus its options with a `format` method, so a caller prints that instead:
// `w.print("hex: {f}\n", .{int.fmt(.hex, .{})})`. This is the shape `std.ascii`
// and `std.zig` use for the same job (`HexEscape`, `FormatId` and its nested
// options struct).
//
// A caller that wants bytes rather than a writer calls `render` with a buffer, which
// is a `Writer.fixed` underneath. Too small a buffer is `error.WriteFailed` rather
// than a walk off the end, which is what the previous buffer-indexing version did.
/// Print as decimal, read by the value's own signedness, without separators: the
/// number, unadorned. `Notation` below covers the deliberate alternates.
pub fn format(self: Integer, w: *Writer) Writer.Error!void {
const notation: Notation = switch (self.signedness) {
.signed => .decimal_signed,
.unsigned => .decimal_unsigned,
};
return self.fmt(notation, .{ .separators = false }).format(w);
}
/// The notations a fixed-width integer renders in.
pub const Notation = enum {
@ -104,8 +107,10 @@ pub const Notation = enum {
decimal_unsigned,
};
/// How a notation is decorated.
pub const Options = struct {
/// How a notation is decorated. Not `std.fmt.Options`, which is the specifier side
/// of a placeholder (`fill`, `width`, `alignment`, `precision`); these are the
/// value's own display choices.
pub const FormatOptions = struct {
/// Group digits for reading: a space per byte in hex and ASCII, per nibble in
/// binary, per three digits in octal, and thousands commas in decimal.
separators: bool = true,
@ -118,29 +123,29 @@ pub const Options = struct {
endian: std.builtin.Endian = .big,
/// What the clipboard wants: no separators, and a prefix where one exists.
pub const plain: Options = .{ .separators = false, .prefix = true };
pub const plain: FormatOptions = .{ .separators = false, .prefix = true };
};
/// This value in `notation`, ready to print.
pub fn as(self: Integer, notation: Notation, options: Options) View {
pub fn fmt(self: Integer, notation: Notation, options: FormatOptions) Format {
return .{ .value = self, .notation = notation, .options = options };
}
pub const View = struct {
pub const Format = struct {
value: Integer,
notation: Notation,
options: Options,
options: FormatOptions,
/// Render into `w`. This is the `{f}` implementation, so `int.as(.hex, .{})`
/// Render into `w`. This is the `{f}` implementation, so `int.fmt(.hex, .{})`
/// prints directly.
pub fn format(self: View, w: *Writer) Writer.Error!void {
pub fn format(self: Format, w: *Writer) Writer.Error!void {
const value = self.value.masked();
switch (self.notation) {
.hex => {
if (self.options.prefix) try w.writeAll("0x");
if (self.options.prefix or !self.options.separators) {
// Canonical, zero-padded to the width: two digits per byte.
try w.printInt(value, 16, .upper, .{ .fill = '0', .width = self.value.bits() / 4 });
try w.printInt(value, 16, .upper, .{ .fill = '0', .width = self.value.width.bits() / 4 });
} else {
// A byte at a time in `endian` order, space-separated. Only the
// byte order swaps; nibbles within a byte are always high-low.
@ -172,7 +177,7 @@ pub const View = struct {
},
.binary => {
if (self.options.prefix) try w.writeAll("0b");
const width: usize = self.value.bits();
const width: usize = self.value.width.bits();
if (!self.options.separators) {
try w.printInt(value, 2, .lower, .{ .fill = '0', .width = width });
} else {
@ -201,13 +206,13 @@ pub const View = struct {
/// Render into `buf` and return what was written. For callers that need a slice,
/// such as the TUI, which measures and positions text before drawing it.
pub fn render(self: View, buf: []u8) Writer.Error![]const u8 {
pub fn render(self: Format, buf: []u8) Writer.Error![]const u8 {
var w = Writer.fixed(buf);
try self.format(&w);
return w.buffered();
}
fn printDecimal(self: View, w: *Writer, value: anytype) Writer.Error!void {
fn printDecimal(self: Format, w: *Writer, value: anytype) Writer.Error!void {
if (!self.options.separators) return w.printInt(value, 10, .lower, .{});
// Grouping works on the digits, so write them first. The widest value is
// minInt(i128): 39 digits and a sign.
@ -219,19 +224,19 @@ pub const View = struct {
/// The byte at display position `k` (0 = leftmost), honouring endianness.
/// Big-endian shows the most significant byte first, little-endian the least.
fn byteAt(self: View, k: usize) u8 {
fn byteAt(self: Format, k: usize) u8 {
const count = self.byteCount();
const msb_index = if (self.options.endian == .big) k else count - 1 - k;
const shift: u7 = @intCast((count - 1 - msb_index) * 8);
return @intCast((self.value.masked() >> shift) & 0xFF);
}
fn byteCount(self: View) usize {
return @as(usize, self.value.bits()) / 8;
fn byteCount(self: Format) usize {
return @as(usize, self.value.width.bits()) / 8;
}
fn octalDigits(self: View) usize {
return (@as(usize, self.value.bits()) + 2) / 3;
fn octalDigits(self: Format) usize {
return (@as(usize, self.value.width.bits()) + 2) / 3;
}
};
@ -365,9 +370,9 @@ fn at(raw: u128, width: BitWidth) Integer {
}
/// Render through a writer, which is the primary path.
fn show(buf: []u8, value: Integer, notation: Notation, options: Options) ![]const u8 {
fn show(buf: []u8, value: Integer, notation: Notation, options: FormatOptions) ![]const u8 {
var w = Writer.fixed(buf);
try w.print("{f}", .{value.as(notation, options)});
try w.print("{f}", .{value.fmt(notation, options)});
return w.buffered();
}
@ -394,7 +399,7 @@ test "hex: display groups per byte, plain form carries the prefix" {
test "hex: little-endian reverses the display, never the prefixed form" {
var buf: [256]u8 = undefined;
const little: Options = .{ .endian = .little };
const little: FormatOptions = .{ .endian = .little };
try testing.expectEqualStrings("EF BE AD DE", try show(&buf, at(0xDEADBEEF, .bits32), .hex, little));
// A prefixed form names the value, so it stays canonical whatever the byte order.
@ -568,12 +573,12 @@ test "render: a buffer too small is an error, not a walk off the end" {
var tiny: [4]u8 = undefined;
try testing.expectError(
error.WriteFailed,
at(0xDEADBEEF, .bits32).as(.binary, .{}).render(&tiny),
at(0xDEADBEEF, .bits32).fmt(.binary, .{}).render(&tiny),
);
// And the successful path returns exactly what was written.
var room: [64]u8 = undefined;
const written = try at(0xFF, .bits8).as(.hex, .{}).render(&room);
const written = try at(0xFF, .bits8).fmt(.hex, .{}).render(&room);
try testing.expectEqualStrings("FF", written);
}
@ -581,6 +586,31 @@ test "a view prints through any writer, which is how the frontends use it" {
var buf: [128]u8 = undefined;
var w = Writer.fixed(&buf);
const value = at(0xDEAD, .bits16);
try w.print("hex: {f} oct: {f}", .{ value.as(.hex, .{}), value.as(.octal, .{}) });
try w.print("hex: {f} oct: {f}", .{ value.fmt(.hex, .{}), value.fmt(.octal, .{}) });
try testing.expectEqualStrings("hex: DE AD oct: 157 255", w.buffered());
}
test "the default format is the number, read by its own signedness" {
// `{f}` on the value itself, for tests, debug output and error messages. No
// separators and no prefix: the notations are how you ask for those.
var buf: [128]u8 = undefined;
var signed = Writer.fixed(&buf);
try signed.print("{f}", .{at(0xFF, .bits8)});
try testing.expectEqualStrings("-1", signed.buffered());
var unsigned = Writer.fixed(&buf);
const as_unsigned: Integer = .{ .raw = 0xFF, .width = .bits8, .signedness = .unsigned };
try unsigned.print("{f}", .{as_unsigned});
try testing.expectEqualStrings("255", unsigned.buffered());
// Wide values print in full, ungrouped.
var wide = Writer.fixed(&buf);
try wide.print("{f}", .{at(1234567, .bits32)});
try testing.expectEqualStrings("1234567", wide.buffered());
// And it composes, which is the point of having it.
var pair = Writer.fixed(&buf);
try pair.print("{f} and {f}", .{ at(5, .bits8), at(0xFE, .bits8) });
try testing.expectEqualStrings("5 and -2", pair.buffered());
}

View file

@ -84,7 +84,7 @@ const Distance = union(enum) {
fn distance(right: Integer) Error!Distance {
if (right.isNegative()) return Error.DomainError;
const value = right.masked();
if (value >= right.bits()) return .past_width;
if (value >= right.width.bits()) return .past_width;
return .{ .within = @intCast(value) };
}
@ -94,7 +94,7 @@ fn distance(right: Integer) Error!Distance {
/// saturated: rotating a 64-bit value by 65 is rotating it by 1.
fn rotation(right: Integer) Error!u7 {
if (right.isNegative()) return Error.DomainError;
return @intCast(right.masked() % right.bits());
return @intCast(right.masked() % right.width.bits());
}
/// Apply a fixed-width operation to two values of the same integer type.
@ -104,7 +104,7 @@ fn rotation(right: Integer) Error!u7 {
pub fn apply(op: Op, left_in: Integer, right_in: Integer) Error!Integer {
std.debug.assert(left_in.sameTypeAs(right_in));
const mask = left_in.mask();
const mask = left_in.width.mask();
const left = left_in.masked();
const right = right_in.masked();
@ -139,13 +139,13 @@ pub fn apply(op: Op, left_in: Integer, right_in: Integer) Error!Integer {
.rotate_left => blk: {
const amt = try rotation(right_in);
if (amt == 0) break :blk left;
const anti: u7 = @intCast(left_in.bits() - amt);
const anti: u7 = @intCast(left_in.width.bits() - amt);
break :blk ((left << amt) | (left >> anti)) & mask;
},
.rotate_right => blk: {
const amt = try rotation(right_in);
if (amt == 0) break :blk left;
const anti: u7 = @intCast(left_in.bits() - amt);
const anti: u7 = @intCast(left_in.width.bits() - amt);
break :blk ((left >> amt) | (left << anti)) & mask;
},
};
@ -299,7 +299,7 @@ test "rotate left and rotate right are inverses at every distance and width" {
.signedness = .unsigned,
};
var amt: u128 = 0;
while (amt < value.bits()) : (amt += 1) {
while (amt < value.width.bits()) : (amt += 1) {
const there = try apply(.rotate_left, value, value.withRaw(amt));
const back = try apply(.rotate_right, there, value.withRaw(amt));
try testing.expectEqual(value.raw, back.raw);
@ -331,7 +331,7 @@ test "not and negate stay inside the width and keep the type" {
}
test "the default value type, which standard mode uses, is 64-bit signed" {
const minus_eight = i64v(@bitCast(@as(i128, -8) & @as(i128, @bitCast(i64v(0).mask()))));
const minus_eight = i64v(@bitCast(@as(i128, -8) & @as(i128, @bitCast(Integer.BitWidth.bits64.mask()))));
try testing.expectEqual(Integer.BitWidth.bits64, minus_eight.width);
try testing.expectEqual(Integer.Signedness.signed, minus_eight.signedness);
// -8 >> 1 is -4 there, which is the case that used to differ between modes.

View file

@ -937,7 +937,7 @@ test "standard mode: its integer type is fixed at 64-bit signed" {
try testing.expectEqual(@as(f64, 1024.0), shifted.toFloat(alloc));
const projected = try standardInt(1);
try testing.expectEqual(@as(u8, 64), projected.bits());
try testing.expectEqual(@as(u8, 64), projected.width.bits());
try testing.expectEqual(Integer.Signedness.signed, projected.signedness);
}

View file

@ -364,7 +364,7 @@ test "displayWidthFor: the chosen width holds the value and sizes the rows" {
const bw = displayWidthFor(value);
try testing.expectEqual(value, value & bw.mask());
const int: Integer = .{ .raw = value, .width = bw, .signedness = .unsigned };
const hex = try int.as(.hex, .{}).render(&buf);
const hex = try int.fmt(.hex, .{}).render(&buf);
// Two hex digits per byte, plus one space between bytes.
const bytes: usize = bw.bits() / 8;
try testing.expectEqual(bytes * 3 - 1, hex.len);

View file

@ -115,7 +115,7 @@ fn evalExpr(config: Config, expr: *const Expr) Error!Integer {
/// apart again. What remains is the arithmetic, which genuinely differs between the
/// modes: it wraps at the width here and is exact rational arithmetic there.
fn evalBinaryOp(op: BinaryOp, left_in: Integer, right_in: Integer) Error!Integer {
const mask = left_in.mask();
const mask = left_in.width.mask();
const left = left_in.masked();
const right = right_in.masked();

View file

@ -497,9 +497,9 @@ fn formatStandardMultiBase(buf: []u8, dec_display: []const u8, value: f64) CliRe
var w = std.Io.Writer.fixed(buf);
w.print("{s}\n hex: {f}\n oct: {f}\n bin: {f}", .{
dec_copy[0..dec_len],
int.as(.hex, .{}),
int.as(.octal, .{}),
int.as(.binary, .{}),
int.fmt(.hex, .{}),
int.fmt(.octal, .{}),
int.fmt(.binary, .{}),
}) catch {
return .{ .output = "error: buffer overflow\n", .is_error = true };
};
@ -517,11 +517,11 @@ fn formatProgrammerResult(buf: []u8, result: engine.Integer, config: engine.prog
\\ bin: {f}
\\
, .{
result.as(.decimal_signed, .{}),
result.as(.decimal_unsigned, .{}),
result.as(.hex, .{ .endian = config.display_endian }),
result.as(.octal, .{}),
result.as(.binary, .{}),
result.fmt(.decimal_signed, .{}),
result.fmt(.decimal_unsigned, .{}),
result.fmt(.hex, .{ .endian = config.display_endian }),
result.fmt(.octal, .{}),
result.fmt(.binary, .{}),
}) catch {
return .{ .output = "error: buffer overflow\n", .is_error = true };
};

View file

@ -1051,9 +1051,9 @@ pub const App = struct {
.signedness = .unsigned,
};
details = .{
try std.fmt.allocPrint(self.allocator, "hex: {f}", .{int.as(.hex, .{})}),
try std.fmt.allocPrint(self.allocator, "oct: {f}", .{int.as(.octal, .{})}),
try std.fmt.allocPrint(self.allocator, "bin: {f}", .{int.as(.binary, .{})}),
try std.fmt.allocPrint(self.allocator, "hex: {f}", .{int.fmt(.hex, .{})}),
try std.fmt.allocPrint(self.allocator, "oct: {f}", .{int.fmt(.octal, .{})}),
try std.fmt.allocPrint(self.allocator, "bin: {f}", .{int.fmt(.binary, .{})}),
};
}
@ -1161,7 +1161,7 @@ pub const App = struct {
fn submitProgrammer(self: *App, expr_text: []const u8) !void {
const is_error, const display_text = if (engine.evalProgrammerString(self.allocator, expr_text, self.prog_config)) |int| blk: {
self.prog_value = int.unsignedValue();
const text = try std.fmt.allocPrint(self.allocator, "{f}", .{int.as(.decimal_unsigned, .{})});
const text = try std.fmt.allocPrint(self.allocator, "{f}", .{int.fmt(.decimal_unsigned, .{})});
break :blk .{ false, text };
} else |err| blk: {
break :blk .{ true, try self.allocator.dupe(u8, errorStr(err)) };

View file

@ -74,7 +74,7 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei
// DEC(s)
var sdec_buf: [256]u8 = undefined;
const sdec = int.as(.decimal_signed, .{}).render(&sdec_buf) catch too_wide;
const sdec = int.fmt(.decimal_signed, .{}).render(&sdec_buf) catch too_wide;
const sdec_style: vaxis.Style = if (focused == .dec_signed)
.{ .fg = C.bg, .bg = C.cyan, .bold = true }
else
@ -85,7 +85,7 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei
// DEC(u)
var udec_buf: [256]u8 = undefined;
const udec = int.as(.decimal_unsigned, .{}).render(&udec_buf) catch too_wide;
const udec = int.fmt(.decimal_unsigned, .{}).render(&udec_buf) catch too_wide;
const udec_style: vaxis.Style = if (focused == .dec_unsigned)
.{ .fg = C.bg, .bg = C.cyan, .bold = true }
else
@ -96,7 +96,7 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei
// HEX
var hex_buf: [256]u8 = undefined;
const hex = int.as(.hex, .{ .endian = app.prog_config.display_endian }).render(&hex_buf) catch too_wide;
const hex = int.fmt(.hex, .{ .endian = app.prog_config.display_endian }).render(&hex_buf) catch too_wide;
const hex_style: vaxis.Style = if (focused == .hex)
.{ .fg = C.bg, .bg = C.green, .bold = true }
else
@ -114,13 +114,13 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei
// ASCII (derived, read-only): one glyph per byte, aligned under HEX.
var ascii_buf: [128]u8 = undefined;
const ascii = int.as(.ascii, .{ .endian = app.prog_config.display_endian }).render(&ascii_buf) catch too_wide;
const ascii = int.fmt(.ascii, .{ .endian = app.prog_config.display_endian }).render(&ascii_buf) catch too_wide;
draw.writeStr(surface, base_start + 3, 2, "ASCII:", .{ .fg = C.cyan });
draw.writeStr(surface, base_start + 3, 11, ascii, .{ .fg = C.orange });
// OCT
var oct_buf: [256]u8 = undefined;
const oct = int.as(.octal, .{}).render(&oct_buf) catch too_wide;
const oct = int.fmt(.octal, .{}).render(&oct_buf) catch too_wide;
const oct_style: vaxis.Style = if (focused == .oct)
.{ .fg = C.bg, .bg = C.purple, .bold = true }
else
@ -136,7 +136,7 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei
// BIN
var bin_buf: [512]u8 = undefined;
const bin = int.as(.binary, .{}).render(&bin_buf) catch too_wide;
const bin = int.fmt(.binary, .{}).render(&bin_buf) catch too_wide;
const bin_style: vaxis.Style = if (focused == .bin)
.{ .fg = C.bg, .bg = C.yellow, .bold = true }
else