| Title: | Automatically Reduce Failing R Scripts to a Minimal Reproducible Example |
| Version: | 0.2.0 |
| Description: | Shrinks a failing R script to the smallest subset of statements that still triggers the same error, using the delta debugging algorithm of Zeller and Hildebrandt (2002) <doi:10.1109/32.988498>. Each candidate reduction is evaluated in a separate R process, so dependencies between statements and their side effects are respected. The result is a one-minimal example, in which removing any remaining statement makes the error disappear; this is the form most useful for bug reports and for questions on community forums. When no statement can be removed, because the failure is nested inside a function body, reduction continues within the surviving statements. A general delta debugging routine and a helper for reducing data frames to the rows that reproduce a failure are also provided. |
| License: | MIT + file LICENSE |
| Encoding: | UTF-8 |
| Imports: | callr, stats, utils |
| Suggests: | clipr, ellmer, knitr, rmarkdown, spelling, testthat (≥ 3.0.0) |
| VignetteBuilder: | knitr |
| Config/testthat/edition: | 3 |
| Language: | en-US |
| URL: | https://github.com/DIGlabUAB/minex |
| BugReports: | https://github.com/DIGlabUAB/minex/issues |
| Config/roxygen2/version: | 8.0.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-08-29 01:25:39 UTC; sumanth |
| Author: | Sandeep Bodduluri |
| Maintainer: | Sumanth Chandrupatla <srchandr@uab.edu> |
| Repository: | CRAN |
| Date/Publication: | 2026-08-29 22:40:02 UTC |
minex: Automatically Reduce Failing R Scripts to a Minimal Reproducible Example
Description
Shrinks a failing R script to the smallest subset of statements that still triggers the same error, using the delta debugging algorithm of Zeller and Hildebrandt (2002) doi:10.1109/32.988498. Each candidate reduction is evaluated in a separate R process, so dependencies between statements and their side effects are respected. The result is a one-minimal example, in which removing any remaining statement makes the error disappear; this is the form most useful for bug reports and for questions on community forums. When no statement can be removed, because the failure is nested inside a function body, reduction continues within the surviving statements. A general delta debugging routine and a helper for reducing data frames to the rows that reproduce a failure are also provided.
Author(s)
Maintainer: Sumanth Chandrupatla srchandr@uab.edu (ORCID) [copyright holder]
Authors:
Sumanth Chandrupatla srchandr@uab.edu (ORCID) [copyright holder]
Sandeep Bodduluri sbodduluri@uabmc.edu (ORCID) [copyright holder]
See Also
Useful links:
Delta debugging
Description
General implementation of the ddmin minimization algorithm of Zeller and
Hildebrandt (2002). Given a collection of elements and a predicate that
reports whether a subset still exhibits some behavior of interest, ddmin()
returns a subset that is one-minimal: the predicate holds for it, but fails
for every subset obtained by removing a single element.
Usage
ddmin(
items,
interesting,
algorithm = c("cdd", "ddmin"),
max_oracle_calls = Inf,
verbose = FALSE,
.info = NULL
)
Arguments
items |
A list or atomic vector of elements to minimize. |
interesting |
A predicate applied to a subset of |
algorithm |
Character. The reduction strategy for the first phase, one
of |
max_oracle_calls |
Numeric. An upper bound on the number of predicate evaluations. Must be at least 1. When the budget is exhausted the reduction stops early and returns the smallest set confirmed so far; the result is still guaranteed to reproduce the behavior but may not be one-minimal. |
verbose |
Logical or character. If |
.info |
Optional environment. When supplied, |
Details
The algorithm partitions the current candidate into n blocks (starting with
n = 2). It first tests whether any single block reproduces the behavior; if
so it continues with that block. Otherwise it tests each complement (the
candidate with one block removed) and continues with the first that
reproduces. If neither succeeds the granularity is doubled, up to the point
where each element sits in its own block, which guarantees one-minimality.
Results of the predicate are cached on the set of element indices, so an identical configuration is never evaluated twice.
Value
The one-minimal subset of items, in the original order.
References
Zeller A, Hildebrandt R (2002). "Simplifying and Isolating Failure-Inducing Input." IEEE Transactions on Software Engineering, 28(2), 183-200. doi:10.1109/32.988498
Zhang M, Xu Z, Tian Y, Cheng X, Sun C (2025). "Toward a Better Understanding of Probabilistic Delta Debugging." ICSE 2025. arXiv:2408.04735. https://arxiv.org/abs/2408.04735
See Also
minex() for the script-reduction front end and reduce_rows() for
reducing data frames.
Examples
# Reduce a sentence to the single word a predicate depends on.
words <- strsplit("the quick brown fox", " ")[[1]]
ddmin(words, function(s) "fox" %in% s)
# When several elements are jointly required, all of them are kept.
nums <- 1:6
ddmin(nums, function(s) sum(s) >= 11 && 6 %in% s)
Explain a minex failure with an LLM
Description
Hands a minex() result's minimal snippet to a language model and returns a
structured explanation: why it fails, a fix (optionally executed to verify it
is gone), a paste-ready bug report, and a diagnosis. Opt-in; requires the
suggested ellmer package and a configured chat backend.
Usage
explain_failure(
x,
chat = NULL,
verify_fix = TRUE,
timeout = 60,
session_info = TRUE,
modes = c("explain", "fix", "report", "diagnose")
)
Arguments
x |
A |
chat |
An |
verify_fix |
Logical. Run the proposed fix to check the failure is gone. |
timeout |
Seconds for the sandboxed fix execution. |
session_info |
Logical. Include |
modes |
Which sections to produce. |
Details
Executing model output: with verify_fix = TRUE (default) the proposed
fix is run in a fresh callr process with a timeout. callr isolates R
session state, not the OS; a fix may still have side effects. Use
verify_fix = FALSE for input you would not run yourself.
Value
A minex_explanation object.
Examples
## Not run:
# Write a failing script and reduce it, then explain the failure.
tmp <- tempfile(fileext = ".R")
writeLines("x <- c(1, 2, NA)\nif (is.na(mean(x))) stop('mean is NA')", tmp)
res <- minex(tmp, backend = "inprocess")
# Requires a configured chat backend (e.g. ellmer::chat_ollama()).
explain_failure(res, chat = ellmer::chat_ollama(model = "llama3.2"),
verify_fix = FALSE)
## End(Not run)
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 |
A character vector of R source lines, or a single string. Takes
precedence over both |
clipboard |
Logical. If |
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 |
Which kind of condition to target: |
match |
How a candidate's failure must match the recorded one when no
|
algorithm |
The reduction strategy passed to |
backend |
Either |
timeout |
Maximum seconds allowed for a single |
max_oracle_calls |
Numeric upper bound on the number of oracle
evaluations in the reduction search, passed to |
verbose |
Logical. If |
granularity |
When |
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)
Reduce a data frame to the rows that reproduce a failure
Description
Often a bug only shows up with a large data frame, even though a handful of
rows is enough to trigger it. reduce_rows() applies ddmin() over the rows
of data and returns the smallest subset for which predicate still holds,
preserving the original row order. The result is typically small enough to
paste into a bug report with dput().
Usage
reduce_rows(
data,
predicate,
algorithm = c("cdd", "ddmin"),
max_oracle_calls = Inf,
verbose = FALSE
)
Arguments
data |
A data frame. |
predicate |
A function taking a data frame (a subset of |
algorithm |
Character. The reduction strategy for the first phase, one
of |
max_oracle_calls |
Numeric. An upper bound on the number of predicate
calls. Passed through to |
verbose |
Logical. If |
Value
A data frame containing the one-minimal subset of rows.
See Also
Examples
df <- data.frame(id = 1:6, value = c(3, 8, 999, 2, 5, 7))
# The failure: any value greater than 100.
reduce_rows(df, function(d) any(d$value > 100))