From 37b3c270204e903b493593c2117d4a6e5b32a293 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 12 Oct 2025 15:11:52 -0500 Subject: [PATCH] synthetic: use std.Io.Writer for more of the interface (#9038) --- src/synthetic/Bytes.zig | 28 +++++++----- src/synthetic/Generator.zig | 14 +++--- src/synthetic/Osc.zig | 88 +++++++++++++++++++++---------------- src/synthetic/Utf8.zig | 23 ++++++---- src/synthetic/cli.zig | 4 +- src/synthetic/cli/Ascii.zig | 4 +- src/synthetic/cli/Osc.zig | 13 +++--- src/synthetic/cli/Utf8.zig | 4 +- 8 files changed, 98 insertions(+), 80 deletions(-) diff --git a/src/synthetic/Bytes.zig b/src/synthetic/Bytes.zig index 8a8207ba9..40a94e0e3 100644 --- a/src/synthetic/Bytes.zig +++ b/src/synthetic/Bytes.zig @@ -27,27 +27,35 @@ pub fn generator(self: *Bytes) Generator { return .init(self, next); } -pub fn next(self: *Bytes, buf: []u8) Generator.Error![]const u8 { +pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { + std.debug.assert(max_len >= 1); const len = @min( self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), - buf.len, + max_len, ); - const result = buf[0..len]; - self.rand.bytes(result); - if (self.alphabet) |alphabet| { - for (result) |*byte| byte.* = alphabet[byte.* % alphabet.len]; + var buf: [8]u8 = undefined; + var remaining = len; + while (remaining > 0) { + const data = buf[0..@min(remaining, buf.len)]; + self.rand.bytes(data); + if (self.alphabet) |alphabet| { + for (data) |*byte| byte.* = alphabet[byte.* % alphabet.len]; + } + try writer.writeAll(data); + remaining -= data.len; } - - return result; } test "bytes" { const testing = std.testing; var prng = std.Random.DefaultPrng.init(0); var buf: [256]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var v: Bytes = .{ .rand = prng.random() }; + v.min_len = buf.len; + v.max_len = buf.len; const gen = v.generator(); - const result = try gen.next(&buf); - try testing.expect(result.len > 0); + try gen.next(&writer, buf.len); + try testing.expectEqual(buf.len, writer.buffered().len); } diff --git a/src/synthetic/Generator.zig b/src/synthetic/Generator.zig index 7478a54c3..28929ecbe 100644 --- a/src/synthetic/Generator.zig +++ b/src/synthetic/Generator.zig @@ -6,27 +6,27 @@ const assert = std.debug.assert; /// For generators, this is the only error that is allowed to be /// returned by the next function. -pub const Error = error{NoSpaceLeft}; +pub const Error = error{WriteFailed}; /// The vtable for the generator. ptr: *anyopaque, -nextFn: *const fn (ptr: *anyopaque, buf: []u8) Error![]const u8, +nextFn: *const fn (ptr: *anyopaque, *std.Io.Writer, usize) Error!void, /// Create a new generator from a pointer and a function pointer. /// This usually is only called by generator implementations, not /// generator users. pub fn init( pointer: anytype, - comptime nextFn: fn (ptr: @TypeOf(pointer), buf: []u8) Error![]const u8, + comptime nextFn: fn (ptr: @TypeOf(pointer), *std.Io.Writer, usize) Error!void, ) Generator { const Ptr = @TypeOf(pointer); assert(@typeInfo(Ptr) == .pointer); // Must be a pointer assert(@typeInfo(Ptr).pointer.size == .one); // Must be a single-item pointer assert(@typeInfo(@typeInfo(Ptr).pointer.child) == .@"struct"); // Must point to a struct const gen = struct { - fn next(ptr: *anyopaque, buf: []u8) Error![]const u8 { + fn next(ptr: *anyopaque, writer: *std.Io.Writer, max_len: usize) Error!void { const self: Ptr = @ptrCast(@alignCast(ptr)); - return try nextFn(self, buf); + try nextFn(self, writer, max_len); } }; @@ -37,6 +37,6 @@ pub fn init( } /// Get the next value from the generator. Returns the data written. -pub fn next(self: Generator, buf: []u8) Error![]const u8 { - return try self.nextFn(self.ptr, buf); +pub fn next(self: Generator, writer: *std.Io.Writer, max_size: usize) Error!void { + try self.nextFn(self.ptr, writer, max_size); } diff --git a/src/synthetic/Osc.zig b/src/synthetic/Osc.zig index 8d5d7d3a2..d78b95a1e 100644 --- a/src/synthetic/Osc.zig +++ b/src/synthetic/Osc.zig @@ -53,6 +53,9 @@ pub fn generator(self: *Osc) Generator { return .init(self, next); } +const osc = std.fmt.comptimePrint("{c}]", .{std.ascii.control_code.esc}); +const st = std.fmt.comptimePrint("{c}", .{std.ascii.control_code.bel}); + /// Get the next OSC request in bytes. The generated OSC request will /// have the prefix `ESC ]` and the terminator `BEL` (0x07). /// @@ -63,23 +66,22 @@ pub fn generator(self: *Osc) Generator { /// /// The buffer must be at least 3 bytes long to accommodate the /// prefix and terminator. -pub fn next(self: *Osc, buf: []u8) Generator.Error![]const u8 { - if (buf.len < 3) return error.NoSpaceLeft; - const unwrapped = try self.nextUnwrapped(buf[2 .. buf.len - 1]); - buf[0] = 0x1B; // ESC - buf[1] = ']'; - buf[unwrapped.len + 2] = 0x07; // BEL - return buf[0 .. unwrapped.len + 3]; +pub fn next(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { + assert(max_len >= 3); + try writer.writeAll(osc); + try self.nextUnwrapped(writer, max_len - (osc.len + st.len)); + try writer.writeAll(st); } -fn nextUnwrapped(self: *Osc, buf: []u8) Generator.Error![]const u8 { +fn nextUnwrapped(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { return switch (self.chooseValidity()) { .valid => valid: { const Indexer = @TypeOf(self.p_valid_kind).Indexer; const idx = self.rand.weightedIndex(f64, &self.p_valid_kind.values); break :valid try self.nextUnwrappedValidExact( - buf, + writer, Indexer.keyForIndex(idx), + max_len, ); }, @@ -87,70 +89,64 @@ fn nextUnwrapped(self: *Osc, buf: []u8) Generator.Error![]const u8 { const Indexer = @TypeOf(self.p_invalid_kind).Indexer; const idx = self.rand.weightedIndex(f64, &self.p_invalid_kind.values); break :invalid try self.nextUnwrappedInvalidExact( - buf, + writer, Indexer.keyForIndex(idx), + max_len, ); }, }; } -fn nextUnwrappedValidExact(self: *const Osc, buf: []u8, k: ValidKind) Generator.Error![]const u8 { - var fbs = std.io.fixedBufferStream(buf); +fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKind, max_len: usize) Generator.Error!void { switch (k) { .change_window_title => { - try fbs.writer().writeAll("0;"); // Set window title + try writer.writeAll("0;"); // Set window title var bytes_gen = self.bytes(); - const title = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(title.len)); + try bytes_gen.next(writer, max_len - 2); }, .prompt_start => { - try fbs.writer().writeAll("133;A"); // Start prompt + try writer.writeAll("133;A"); // Start prompt // aid if (self.rand.boolean()) { var bytes_gen = self.bytes(); bytes_gen.max_len = 16; - try fbs.writer().writeAll(";aid="); - const aid = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(aid.len)); + try writer.writeAll(";aid="); + try bytes_gen.next(writer, max_len); } // redraw if (self.rand.boolean()) { - try fbs.writer().writeAll(";redraw="); + try writer.writeAll(";redraw="); if (self.rand.boolean()) { - try fbs.writer().writeAll("1"); + try writer.writeAll("1"); } else { - try fbs.writer().writeAll("0"); + try writer.writeAll("0"); } } }, - .prompt_end => try fbs.writer().writeAll("133;B"), // End prompt + .prompt_end => try writer.writeAll("133;B"), // End prompt } - - return fbs.getWritten(); } fn nextUnwrappedInvalidExact( self: *const Osc, - buf: []u8, + writer: *std.Io.Writer, k: InvalidKind, -) Generator.Error![]const u8 { + max_len: usize, +) Generator.Error!void { switch (k) { .random => { var bytes_gen = self.bytes(); - return try bytes_gen.next(buf); + try bytes_gen.next(writer, max_len); }, .good_prefix => { - var fbs = std.io.fixedBufferStream(buf); - try fbs.writer().writeAll("133;"); + try writer.writeAll("133;"); var bytes_gen = self.bytes(); - const data = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(data.len)); - return fbs.getWritten(); + try bytes_gen.next(writer, max_len - 4); }, } } @@ -177,11 +173,21 @@ const Validity = enum { valid, invalid }; const test_seed = 0xC0FFEEEEEEEEEEEE; test "OSC generator" { + const testing = std.testing; var prng = std.Random.DefaultPrng.init(test_seed); - var buf: [4096]u8 = undefined; - var v: Osc = .{ .rand = prng.random() }; - const gen = v.generator(); - for (0..50) |_| _ = try gen.next(&buf); + var buf: [256]u8 = undefined; + { + var v: Osc = .{ + .rand = prng.random(), + }; + const gen = v.generator(); + for (0..50) |_| { + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const result = writer.buffered(); + try testing.expect(result.len > 0); + } + } } test "OSC generator valid" { @@ -195,7 +201,9 @@ test "OSC generator valid" { .p_valid = 1.0, }; for (0..50) |_| { - const seq = try gen.next(&buf); + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const seq = writer.buffered(); var parser: terminal.osc.Parser = .init(); for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) != null); @@ -213,7 +221,9 @@ test "OSC generator invalid" { .p_valid = 0.0, }; for (0..50) |_| { - const seq = try gen.next(&buf); + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const seq = writer.buffered(); var parser: terminal.osc.Parser = .init(); for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) == null); diff --git a/src/synthetic/Utf8.zig b/src/synthetic/Utf8.zig index c3ace6505..0d72a8bb2 100644 --- a/src/synthetic/Utf8.zig +++ b/src/synthetic/Utf8.zig @@ -41,13 +41,12 @@ pub fn generator(self: *Utf8) Generator { return .init(self, next); } -pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { +pub fn next(self: *Utf8, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { const len = @min( self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), - buf.len, + max_len, ); - const result = buf[0..len]; var rem: usize = len; while (rem > 0) { // Pick a utf8 byte count to generate. @@ -75,9 +74,11 @@ pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { assert(std.unicode.utf8CodepointSequenceLength( cp, ) catch unreachable == @intFromEnum(utf8_len)); - rem -= std.unicode.utf8Encode( + + var buf: [4]u8 = undefined; + const l = std.unicode.utf8Encode( cp, - result[result.len - rem ..], + &buf, ) catch |err| switch (err) { // Impossible because our generation above is hardcoded to // produce a valid range. If not, a bug. @@ -86,18 +87,22 @@ pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { // Possible, in which case we redo the loop and encode nothing. error.Utf8CannotEncodeSurrogateHalf => continue, }; + try writer.writeAll(buf[0..l]); + rem -= l; } - - return result; } test "utf8" { const testing = std.testing; var prng = std.Random.DefaultPrng.init(0); var buf: [256]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var v: Utf8 = .{ .rand = prng.random() }; + v.min_len = buf.len; + v.max_len = buf.len; const gen = v.generator(); - const result = try gen.next(&buf); - try testing.expect(result.len > 0); + try gen.next(&writer, buf.len); + const result = writer.buffered(); + try testing.expectEqual(256, result.len); try testing.expect(std.unicode.utf8ValidateSlice(result)); } diff --git a/src/synthetic/cli.zig b/src/synthetic/cli.zig index b32469aab..d9b6a659d 100644 --- a/src/synthetic/cli.zig +++ b/src/synthetic/cli.zig @@ -100,7 +100,9 @@ fn mainActionImpl( try impl.run(writer, rand); // Always flush - try writer.flush(); + writer.flush() catch |err| switch (err) { + error.WriteFailed => return, + }; } test { diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig index 339bdee2e..b2d57fa88 100644 --- a/src/synthetic/cli/Ascii.zig +++ b/src/synthetic/cli/Ascii.zig @@ -31,10 +31,8 @@ pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { .alphabet = synthetic.Bytes.Alphabet.ascii, }; - var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { + gen.next(writer, 1024) catch |err| { const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed diff --git a/src/synthetic/cli/Osc.zig b/src/synthetic/cli/Osc.zig index 23d19e4ae..8250b81de 100644 --- a/src/synthetic/cli/Osc.zig +++ b/src/synthetic/cli/Osc.zig @@ -37,14 +37,11 @@ pub fn run(self: *Osc, writer: *std.Io.Writer, rand: std.Random) !void { var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { - const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); - switch (@as(Error, err)) { - error.BrokenPipe => return, // stdout closed - error.WriteFailed => return, // fixed buffer full - else => return err, - } + var fixed: std.Io.Writer = .fixed(&buf); + try gen.next(&fixed, buf.len); + const data = fixed.buffered(); + writer.writeAll(data) catch |err| switch (err) { + error.WriteFailed => return, }; } } diff --git a/src/synthetic/cli/Utf8.zig b/src/synthetic/cli/Utf8.zig index 3c2fddef7..635704755 100644 --- a/src/synthetic/cli/Utf8.zig +++ b/src/synthetic/cli/Utf8.zig @@ -30,10 +30,8 @@ pub fn run(self: *Utf8, writer: *std.Io.Writer, rand: std.Random) !void { .rand = rand, }; - var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { + gen.next(writer, 1024) catch |err| { const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed