minex: Minimize a failing R script to a reproducible example

View source: R/minex.R

minexR Documentation

Minimize a failing R script to a reproducible example

Description

Reduces a failing piece of R code to the smallest subset of its top-level statements that still triggers the same failure. The result is a one-minimal example: removing any remaining statement makes the failure disappear. This is the form requested when reporting bugs or asking for help, and the part of preparing such an example that is usually done by hand.

Usage

minex(
  file = NULL,
  code = NULL,
  clipboard = FALSE,
  oracle = NULL,
  condition = c("error", "warning", "message", "any"),
  match = c("message", "class", "both"),
  algorithm = c("cdd", "ddmin"),
  backend = c("callr", "inprocess"),
  timeout = 60,
  max_oracle_calls = Inf,
  verbose = FALSE,
  granularity = c("statement", "expression")
)

Arguments

file

Path to a file containing the R code to minimize. Used only when neither code nor clipboard is supplied.

code

A character vector of R source lines, or a single string. Takes precedence over both clipboard and file.

clipboard

Logical. If TRUE, read the R code to minimize from the system clipboard. Takes precedence over file, but is overridden by code. Input precedence is ⁠code > clipboard > file⁠; supplying more than one source emits a warning and the highest-precedence source is used.

oracle

Optional predicate taking a character vector of statements and returning a single logical. When supplied, the target failure is not recorded automatically and condition, match and failure-point truncation are all bypassed; you are fully responsible for defining what counts as reproducing the failure.

condition

Which kind of condition to target: "error" (the default), "warning", "message", or "any" (the most severe condition present, error > warning > message). Ignored when a custom oracle is supplied.

match

How a candidate's failure must match the recorded one when no oracle is given: "message" (identical message, the default), "class" (shares a condition class) or "both". Matching on the message is usually right, because an over-reduced fragment tends to fail with a different message (for example "object not found"). Can also be a function taking two condition summaries, candidate and target (each a list with message and classes), and returning a single logical, for custom matching logic.

algorithm

The reduction strategy passed to ddmin(): "cdd" (convergent delta debugging, the default) or "ddmin" (the classic block-halving loop).

backend

Either "callr" (evaluate each candidate in a fresh R process, the default and the only choice that fully isolates state) or "inprocess" (evaluate in the current session, faster but without isolation; a script's side effects other than options are not sandboxed). Soundness caveat: "inprocess" evaluates in an environment chained to the caller's global environment, so a candidate that references a name which happens to exist in the caller's workspace resolves it instead of raising ⁠object not found⁠. Over-reduction can therefore spuriously still "succeed" against that tripwire. backend = "callr" (the default) evaluates in a clean process and is unaffected; prefer "inprocess" only for trusted, quick iteration.

timeout

Maximum seconds allowed for a single callr evaluation.

max_oracle_calls

Numeric upper bound on the number of oracle evaluations in the reduction search, passed to ddmin() (and to the HDD pass when granularity = "expression"). When the budget is exhausted the reduction stops early with a warning; the result still reproduces the failure but may not be one-minimal. The one-time failure-point truncation probe is mandatory setup, exempt from this limit, but is still included in the reported oracle_calls. The budget bounds the whole call: when the statement pass escalates to "expression" (see granularity), the second pass runs on whatever the first left rather than on a fresh budget.

verbose

Logical. If TRUE, report progress. If "trace", also populate the result's trace with a per-oracle-call record. For granularity = "expression", the HDD trace rows additionally carry stmt_index (which statement they reduced) and level (the HDD tree depth); the statement-level rows have NA in both. For granularity = "statement" the trace is unchanged from 0.2.0 (no stmt_index/level columns).

granularity

"statement" (the default) reduces only at the level of top-level statements. "expression" additionally reduces within each surviving statement via HDD (hierarchical delta debugging): pipeline stages (⁠|>⁠, magrittr ⁠%>%⁠) and positional call arguments can be dropped from a statement as long as the whole kept set still reproduces the target failure. When such a reduction makes an earlier statement redundant (for example a value dropped from a later call), that statement is swept out, so the result stays statement one-minimal.

When granularity is left at its default and statement-level reduction removes nothing, minex() retries once at "expression" rather than returning the input unchanged. This is the common case for a script that is one function definition plus a call, where every top-level statement is load-bearing but the failure is nested inside the function body. The retried result carries escalated_from = "statement", and its code is a simplification of the original statements rather than a subset of them. Passing granularity = "statement" explicitly suppresses the retry and reduces at statement level only. No retry happens when the statement pass stopped early against max_oracle_calls, since it has not then shown that nothing is removable.

Details

When no subset of the top-level statements can be removed – the usual case when the failure is nested inside a function body – minex() continues within the surviving statements instead of returning the input unchanged, so the reduced code is then a simplification of the original statements rather than a subset of them. See granularity.

By default minex() first runs the whole input to record the failure it produces (its condition message and class), then uses ddmin() to search for a minimal subset that reproduces it. Each candidate is evaluated in a separate R process so that dependencies between statements and their side effects are respected; removing a statement that a later one needs typically changes the error, and such a removal is therefore rejected.

Supply a custom oracle to minimize against any condition you can express as a predicate, rather than against the recorded failure. The oracle receives a character vector of statements and must return a single logical.

Value

An object of class "minex_result": a list with the minimized code (a character vector of statements), the original statements, the statement counts n_original and n_minimal, the character counts n_chars_original and n_chars_minimal (nchar() of the code collapsed to a single string, before and after reduction), the number of oracle_calls (every predicate evaluation, including the failure-point truncation probe), the recorded target failure (or NULL for a custom oracle), the granularity setting used, and the match and backend settings.

When the statement pass escalated to "expression", the result also carries escalated_from = "statement" and coarse_oracle_calls, the share of oracle_calls spent on the discarded statement-level pass. Both are absent otherwise, so is.null(res$escalated_from) distinguishes a result that was reduced at the granularity asked for from one that had to descend.

See Also

ddmin() for the underlying algorithm and reduce_rows() for reducing data frames.

Examples

# A failing script padded with irrelevant setup.
script <- c(
  "a <- 10",
  "b <- 20",
  "log('not a number')"
)
res <- minex(code = script, backend = "inprocess")
res
cat(as.character(res), "\n")

# A failure that genuinely depends on an earlier statement: both are kept.
script2 <- c(
  "x <- c(1, 2, NA)",
  "m <- mean(x)",
  "if (is.na(m)) stop('mean is NA')"
)
minex(code = script2, backend = "inprocess")


# The default backend runs candidates in fresh R processes.
minex(code = script)


## Not run: 
# Reduce a failing script copied to the system clipboard:
minex(clipboard = TRUE)

## End(Not run)

minex documentation built on Aug. 30, 2026, 1:07 a.m.