When you ask for help with an R problem, the standard advice is to post a minimal reproducible example. The reprex package handles the reproducible half: it runs your snippet in a clean session and formats the code together with its output. It does nothing about the minimal half. That part, stripping a script down to the few lines that actually matter, is still done by hand, and it is the tedious step.
minex() automates it. Give it failing code and it
returns the smallest subset of statements that still produces the same
error.
Here is a script where only the last line is to blame, surrounded by setup that has nothing to do with the failure.
script <- c(
"a <- 10",
"b <- 20",
"log('not a number')"
)
res <- minex(code = script, backend = "inprocess")
res
#> <minex_result> 3 statement(s) reduced to 1 (3 oracle call(s))
#> target failure: non-numeric argument to mathematical function
#> ------------------------------------------------
#> log('not a number')The setup lines are gone. as.character() gives you the
bare code, ready to paste into a bug report.
(The examples here use backend = "inprocess" so that
they run quickly inside the vignette. In normal use the default
backend = "callr" evaluates each candidate in a fresh R
process, which is what you want when statements have side effects.)
Reduction never throws away a statement the failure depends on. When a later line needs an earlier one, removing the earlier line changes the error, and the oracle rejects that reduction. Both lines below survive because they are jointly required.
script <- c(
"x <- c(1, 2, NA)",
"m <- mean(x)",
"if (is.na(m)) stop('mean is NA')"
)
minex(code = script, backend = "inprocess")
#> <minex_result> 3 statement(s) reduced to 3 (7 oracle call(s))
#> target failure: mean is NA
#> ------------------------------------------------
#> x <- c(1, 2, NA)
#> m <- mean(x)
#> if (is.na(m)) stop('mean is NA')By default a candidate must fail with the same condition message as the original. This is usually the right choice: an over-reduced fragment tends to fail differently (often “object not found”), and message matching rejects it.
If the message embeds changing details such as a value or an index, match on the condition class instead:
minex(code = script, match = "class", backend = "inprocess")
#> <minex_result> 3 statement(s) reduced to 1 (3 oracle call(s))
#> target failure: mean is NA
#> ------------------------------------------------
#> if (is.na(m)) stop('mean is NA')Use match = "both" to require the message and a shared
class.
For full control, pass an oracle: a predicate over the
statements that returns TRUE when they still reproduce
whatever you care about. With a custom oracle, minex() does
not record a target failure and match is ignored.
A bug often hides in a large data frame even though a couple of rows
are enough to trigger it. reduce_rows() runs the same
search over rows.
df <- data.frame(id = 1:6, value = c(3, 8, 999, 2, 5, 7))
reduce_rows(df, function(d) any(d$value > 100))
#> id value
#> 3 3 999The result is usually small enough to capture with
dput().
Both minex() and reduce_rows() are front
ends to ddmin(), an implementation of the delta debugging
algorithm of Zeller and Hildebrandt (2002). ddmin() works
on any collection plus a predicate, so it is reusable on its own.
It partitions the candidate into blocks, keeps the smallest block (or complement) that still reproduces the behavior, and increases the granularity until every remaining element is load-bearing. The output is one-minimal: removing any single element makes the behavior disappear.
minex() targets R-level conditions (errors). Failures
that crash the R process or hang are not captured as conditions; with
the callr backend they are simply treated as not
reproducing the target. Reduction granularity is the top-level
statement, so it will not reach inside a single large expression.