synthetic: use std.Io.Writer for more of the interface (#9038)

pull/9170/head
Jeffrey C. Ollie 2025-10-12 15:11:52 -05:00 committed by GitHub
parent 03e71e86a4
commit 37b3c27020
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 98 additions and 80 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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));
}

View File

@ -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 {

View File

@ -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

View File

@ -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,
};
}
}

View File

@ -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