diff --git a/engine/src/Integer.zig b/engine/src/Integer.zig index bb8f1ed..d919815 100644 --- a/engine/src/Integer.zig +++ b/engine/src/Integer.zig @@ -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()); +} diff --git a/engine/src/bitwise.zig b/engine/src/bitwise.zig index fcae777..6fb2158 100644 --- a/engine/src/bitwise.zig +++ b/engine/src/bitwise.zig @@ -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. diff --git a/engine/src/evaluator.zig b/engine/src/evaluator.zig index 6fc63b7..26303ae 100644 --- a/engine/src/evaluator.zig +++ b/engine/src/evaluator.zig @@ -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); } diff --git a/engine/src/formatter.zig b/engine/src/formatter.zig index b9179e5..26c5681 100644 --- a/engine/src/formatter.zig +++ b/engine/src/formatter.zig @@ -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); diff --git a/engine/src/programmer.zig b/engine/src/programmer.zig index 61d29a7..a6d3135 100644 --- a/engine/src/programmer.zig +++ b/engine/src/programmer.zig @@ -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(); diff --git a/src/main.zig b/src/main.zig index da74151..056bbee 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 }; }; diff --git a/src/tui.zig b/src/tui.zig index 7ffb32c..b41527b 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -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)) }; diff --git a/src/tui/programmer.zig b/src/tui/programmer.zig index 74db5a2..a5ad909 100644 --- a/src/tui/programmer.zig +++ b/src/tui/programmer.zig @@ -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