diff --git a/pbt/mst.zig b/pbt/mst.zig index dfd20da..3dd1d5b 100644 --- a/pbt/mst.zig +++ b/pbt/mst.zig @@ -15,8 +15,12 @@ //! run with `zig build pbt-mst -Dhegel=`. see //! ../hegel-zig for `HEGEL_TEST_CASES`, `HEGEL_SEED`, and `HEGEL_ZIG_ECHO`. //! `PBT_MST_MIN_STEPS` and `PBT_MST_MAX_STEPS` bound the length of a history -//! (default 0 to 40). libhegel favors short histories, so raise the minimum to -//! spend a run on long ones. `PBT_MST_POOL` picks a different key pool, which +//! (default 0 to 40). by default each step draws from a weighted table over +//! an engine-sized sequence, which averages about 7 steps whatever the bound; +//! raise the minimum to spend a run on long histories. `PBT_MST_ENGINE=machine` +//! hands operation choice to libhegel's state machine instead: histories run +//! close to the maximum, with swarm testing, at several times the cost per +//! case (the minimum is ignored). `PBT_MST_POOL` picks a different key pool, which //! changes how the layers interleave in key order. `PBT_MST_KNOWN` ends a //! history quietly when it reaches a known finding (see `refused`), so that a //! run can search past it. @@ -53,6 +57,7 @@ const Fixture = struct { min_steps: u64, max_steps: u64, skip_known: bool, + engine_machine: bool, fn init(gpa: Allocator) !Fixture { const pool: usize = @intCast(envInt("PBT_MST_POOL") orelse 0); @@ -84,6 +89,10 @@ const Fixture = struct { value.* = try Cid.forDagCbor(gpa, try std.fmt.allocPrint(gpa, "pbt.mst.value/{d}", .{i})); } fixture.skip_known = std.c.getenv("PBT_MST_KNOWN") != null; + fixture.engine_machine = if (std.c.getenv("PBT_MST_ENGINE")) |engine| + std.mem.eql(u8, std.mem.span(engine), "machine") + else + false; fixture.min_steps = envInt("PBT_MST_MIN_STEPS") orelse 0; fixture.max_steps = @max(fixture.min_steps, envInt("PBT_MST_MAX_STEPS") orelse 40); return fixture; @@ -134,6 +143,37 @@ const op_weights = [_]Op{ .reload_bare, }; +/// the same weights for libhegel's own state machine, in `Op` tag order +const machine_weights = blk: { + var weights: [std.meta.tags(Op).len]f64 = @splat(0); + for (op_weights) |op| weights[@backingInt(op)] += 1; + break :blk weights; +}; + +/// who picks the next operation. `table` draws an index into `op_weights` +/// for each step of an engine-sized sequence. `machine` hands selection to +/// libhegel's state machine, which adds swarm testing and its own stopping rule. +const Driver = union(enum) { + table: hegel.Steps, + machine: hegel.Machine(Op), + + fn next(self: *Driver, case: *hegel.TestCase) !?Op { + switch (self.*) { + .table => |*steps| { + if (!try steps.next()) return null; + return op_weights[try case.index(op_weights.len)]; + }, + .machine => |*machine| return machine.next(), + } + } + + fn deinit(self: *Driver) void { + switch (self.*) { + inline else => |*driver| driver.deinit(), + } + } +}; + const Machine = struct { fixture: *const Fixture, case: *hegel.TestCase, @@ -146,8 +186,11 @@ const Machine = struct { fn run(self: *Machine) !void { const case = self.case; - var steps = try case.steps(self.min_steps, self.max_steps); - defer steps.deinit(); + var driver: Driver = if (self.fixture.engine_machine) + .{ .machine = try case.machine(Op, &machine_weights, @intCast(self.max_steps)) } + else + .{ .table = try case.steps(self.min_steps, self.max_steps) }; + defer driver.deinit(); tally.cases += 1; // a uniformly drawn starting set gives large trees from the first step, @@ -163,8 +206,7 @@ const Machine = struct { } } - while (try steps.next()) { - const op = op_weights[try case.index(op_weights.len)]; + while (try driver.next(case)) |op| { tally.steps += 1; tally.ops[@backingInt(op)] += 1; const t = try self.drawLiveSlot(); @@ -415,7 +457,8 @@ pub fn main() !void { const fixture = try Fixture.init(arena.allocator()); fixture.describe(); - // libhegel's default of 100 cases misses planted dirty-flag bugs that 5000 finds + // 100 cases catch each planted dirty-flag bug in 9 or 10 runs of 10; this + // is headroom for bugs nobody planted const test_cases: ?u64 = if (envInt("HEGEL_TEST_CASES") == null) 5000 else null; const outcome = hegel.run(.{ .test_cases = test_cases }, &fixture, property);