Skip to documentation
SLOP

tiny.reducer.bytes.reduction

Reference tiny.reducer bytes reduction

Defined in bytes.

The reduction loop, the counters a run reports, and the borrowed result.

API (2)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callsbytesreduction
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Source: lib/reducer/src/bytes/reduce.zig

zig
//! The reduction loop, the counters a run reports, and the borrowed result.//!//! The search looks for a shorter subsequence that still induces the failure,//! by deleting adjacent chunks of bytes and asking the caller's oracle about//! each result.//!//! ## Algorithmic Structure//!//! The published `ddmin` of Zeller and Hildebrandt, 2002, Figure 5, splits an//! input into $n$ subsets $\Delta_1, \dots, \Delta_n$ and alternates two//! phases: one tests each subset $\Delta_i$ on its own, the other tests the//! complement $c \setminus \Delta_i$, with $n$ moving between 2 and $|c|$.//!//! This implementation departs from that in three ways://! - **Omits a separate subset-testing phase:** it runs contiguous deletion//!   sweeps through `without()`. Deleting one half of a two-way split leaves a//!   complement slice that coincides with the other half, and that case arrives//!   through deletion.//! - **Controls search by byte chunk size:** the search starts with the chunk//!   at the whole length of the current sequence, which deletes the entire//!   input and tests the empty sequence `""`, and each later pass halves the//!   chunk with ceiling division, down to a chunk of 1://!   $$\text{chunk} \leftarrow \lfloor \text{chunk} / 2 \rfloor + (\text{chunk} \bmod 2)$$//! - **Restarts greedily at full length:** the moment the oracle answers//!   interesting for a candidate, that candidate replaces the current sequence,//!   the reduction counter goes up by one, and the chunk resets to the whole//!   length of the newly shortened sequence, which starts the coarse deletion//!   search again from the beginning.//!//! ## Memory Movement and Double-Buffering//!//! Once the workspace is acquired, the loop calls no allocator of its own.//!//! - **Candidate construction**: assembling a candidate in `without()` copies//!   bytes from the current sequence into the scratch lane, as the two pieces//!   `target[0..start]` and `target[start..]`.//! - **Candidate acceptance**: when the oracle accepts a candidate, the two//!   local names swap, which is written `spare = current; current = candidate;`//!   in the code, and that swap of slices copies no bytes back.//!//! `Storage.current` and `Storage.candidate` are fixed regions of the backing//! buffer, so swapping the local names leaves the accepted witness in//! `Result.bytes` sitting in either one of them.//!//! The loop allocates nothing, while a run as a whole allocates whatever the//! caller's oracle allocates.const std = @import("std");const model = @import("model.zig");const storage_mod = @import("storage.zig");/// Outcome of one run, borrowing its memory from the acquired workspace.////// ## Lifetime and Borrowing Contract////// The `bytes` slice points into the backing buffer of `storage`, and it stays/// readable while the result is still live and the workspace has been neither/// reused nor torn down.////// `deinit()` gives up this run's lease by calling `storage.release()`, which/// returns the workspace to steady and idle so a later run can acquire it, and/// it moves the workspace no closer to teardown and frees no memory.////// `deinit()` also clears the result's own fields, setting `bytes = &.{}`,/// `attempts = 0`, `reductions = 0`, and `completion = .budget_exhausted`, and/// it is called exactly once per result.////// A caller that needs the reduced bytes after the workspace is reused or torn/// down, or that wants to feed them in as the next run's input on the same/// workspace, copies them into a buffer of its own before calling `deinit()`.pub const Result = struct {    /// Borrowed slice holding the reduced byte sequence, pointing into the    /// workspace.    bytes: []const u8,    /// Count of every oracle call the run made, including the first call, which    /// checks the caller's own input. On a live result, before `deinit()`, the    /// count is at least 1. `deinit()` sets it to 0.    attempts: usize,    /// Count of the candidate deletions the oracle accepted. Each accepted    /// reduction makes the witness strictly shorter. `deinit()` sets it to 0.    reductions: usize,    /// Search outcome status flag holding `.one_minimal` when every single-byte    /// occurrence deletion was put to the oracle and rejected. It holds    /// `.budget_exhausted` when the run stopped because `attempts` reached    /// `max_attempts` before the single-byte sweep finished. `deinit()` sets it    /// to `.budget_exhausted`.    completion: model.Completion,    /// Pointer to the workspace that owns the memory `bytes` refers to.    storage: *storage_mod.Storage,    /// Gives up this run's lease on the workspace, returning it to steady and    /// idle.    ///    /// After the call, `bytes` is an empty slice, the counters are zero, and    /// the workspace can be acquired again.    ///    /// ## Safety    ///    /// - It is called exactly once per result.    /// - One owner holds a given result and makes no shallow copy of it.    /// - The result's public bookkeeping fields stay as the run left them.    pub fn deinit(self: *Result) void {        self.storage.release();        self.bytes = &.{};        self.attempts = 0;        self.reductions = 0;        self.completion = .budget_exhausted;    }};/// Shortens an initial byte sequence against the caller's failure predicate.////// ## Preconditions////// - `storage` sits in the `alloc_phase.capacity.Phase.steady` phase, which///   `storage.activate()` reaches after `Storage.init()`./// - No other run holds `storage`, so `storage.status().in_use` is false./// - `initial.len` is at most `storage.status().max_input_bytes`./// - The memory of `initial` stays valid and unchanged for the whole call./// - The oracle answers interesting for `initial`./// - `settings.max_attempts` is above 0.////// ## Execution Semantics and Error Isolation////// 1. A `settings.max_attempts` of 0 returns `error.AttemptBudgetExhausted` at///    once, without reaching for the workspace, so an earlier run's hold on the///    workspace stands untouched./// 2. The call then acquires the workspace. A workspace another run already///    holds returns `error.ReductionStorageInUse`, and an `initial.len` above///    the capacity returns `error.InputCapacityExceeded`. Both return before///    any workspace byte changes, so a call refused as busy leaves an earlier///    live `Result` holding its lease./// 3. Once the workspace is acquired, `errdefer storage.release()` gives up///    this run's lease on any later failure. Giving up the lease restores///    neither the scratch bytes the run overwrote nor any side effect the///    callback produced./// 4. The oracle calls start with attempt 1 on `initial`, where the oracle///    receives the caller's own slice before any copy. An `initial` the oracle///    answers uninteresting for returns `error.InitialInputUninteresting` and///    gives up the lease./// 5. The call then tests chunk deletions of the current chunk size, starting///    at `current.len` and halving with ceiling division down to 1. Each trial///    candidate is built in the scratch lane by copying the bytes that survive///    the deletion, through `without()`./// 6. When the oracle accepts a candidate, the locals `current` and `spare`///    swap without copying bytes back, `reductions` goes up by one, and `chunk`///    resets to the new `current.len`./// 7. The call terminates on one of three conditions:///    - A full pass at a chunk of 1 that finishes with every unit deletion///      rejected sets `completion = .one_minimal`, and that final sweep may///      finish on the exact last allowed attempt.///    - A `current.len` of 0 sets `completion = .one_minimal`.///    - An `attempts` count that reaches `settings.max_attempts` leaves///      `completion = .budget_exhausted`.////// ## Memory and Allocation////// Once the workspace is acquired, the loop calls no allocator of its own, the/// workspace owns every byte the run touches, and the memory the callback/// allocates along with its side effects sit outside that account.////// ## Errors////// It returns `Error`, which is `Exhaustion || InputError`, or any error the/// oracle returned. Only an error raised after the workspace was acquired gives/// up the lease.////// ## Example////// ```zig/// const std = @import("std");/// const reducer = @import("reducer");////// const Context = struct { needle: []const u8 };////// fn checkContains(input: []const u8, ctx_ptr: *anyopaque) anyerror!reducer.Interesting {///     const ctx: *const Context = @ptrCast(@alignCast(ctx_ptr));///     return if (std.mem.indexOf(u8, input, ctx.needle) != null)///         .interesting///     else///         .uninteresting;/// }////// test "basic reduce invocation" {///     const allocator = std.testing.allocator;///     const initial_input = "prefix_ERR_suffix";///     var storage = try reducer.Storage.init(allocator, .{///         .max_input_bytes = initial_input.len,///     });///     defer storage.deinit(allocator);///     storage.activate();//////     var ctx = Context{ .needle = "ERR" };///     var res = try reducer.reduce(&storage, initial_input, checkContains, &ctx, .{});///     defer res.deinit();//////     try std.testing.expectEqualSlices(u8, "ERR", res.bytes);///     try std.testing.expectEqual(reducer.Completion.one_minimal, res.completion);/// }/// ```pub fn reduce(    storage: *storage_mod.Storage,    initial: []const u8,    interesting_fn: model.InterestingFn,    context: *anyopaque,    settings: model.Settings,) anyerror!Result {    if (settings.max_attempts == 0) return error.AttemptBudgetExhausted;    const regions = try storage.acquire(initial.len);    errdefer storage.release();    std.debug.assert(regions.current.len == initial.len);    std.debug.assert(regions.candidate.len >= initial.len -| 1);    var attempts: usize = 1;    if (try interesting_fn(initial, context) == .uninteresting) {        return error.InitialInputUninteresting;    }    @memcpy(regions.current, initial);    var current = regions.current;    var spare = regions.candidate;    var reductions: usize = 0;    var chunk = current.len;    var completion: model.Completion =        if (current.len == 0) .one_minimal else .budget_exhausted;    while (chunk > 0 and attempts < settings.max_attempts) {        var improved = false;        var sweep_finished = false;        var start: usize = 0;        while (start < current.len and attempts < settings.max_attempts) {            const end = if (chunk > current.len - start) current.len else start + chunk;            const candidate = without(spare, current, start, end);            attempts += 1;            const candidate_status = try interesting_fn(candidate, context);            if (candidate_status == .interesting) {                std.debug.assert(candidate.len < current.len);                spare = current;                current = candidate;                reductions += 1;                chunk = current.len;                improved = true;                if (current.len == 0) completion = .one_minimal;                break;            }            if (end == current.len) {                sweep_finished = true;                break;            }            start += chunk;        }        if (improved) continue;        if (chunk == 1) {            if (sweep_finished) completion = .one_minimal;            break;        }        if (!sweep_finished) break;        chunk = chunk / 2 + chunk % 2;    }    std.debug.assert(attempts <= settings.max_attempts);    std.debug.assert(current.len <= initial.len);    if (completion == .one_minimal and current.len > 0) {        std.debug.assert(chunk == 1);    }    return makeResult(storage, current, attempts, reductions, completion);}fn makeResult(    storage: *storage_mod.Storage,    bytes: []const u8,    attempts: usize,    reductions: usize,    completion: model.Completion,) Result {    std.debug.assert(storage.status().in_use);    std.debug.assert(attempts > 0);    return .{        .bytes = bytes,        .attempts = attempts,        .reductions = reductions,        .completion = completion,        .storage = storage,    };}fn without(target: []u8, source: []const u8, start: usize, end: usize) []u8 {    std.debug.assert(start <= end);    std.debug.assert(end <= source.len);    std.debug.assert(target.len >= source.len - (end - start));    const candidate = target[0 .. source.len - (end - start)];    @memcpy(candidate[0..start], source[0..start]);    @memcpy(candidate[start..], source[end..]);    return candidate;}

Source: lib/reducer/src/bytes/root.zig:52

zig
/// Module holding the reduction loop, the execution counters, and the borrowed/// result.pub const reduction = @import("reduce.zig");

Audit

Definitions1
Public names1
Members0
Version26.7.0
Revisiondaab053ee433