--- title: "Minimizing failing R code with minex" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Minimizing failing R code with minex} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(minex) ``` ## The gap minex fills When you ask for help with an R problem, the standard advice is to post a *minimal reproducible example*. The [reprex](https://cran.r-project.org/package=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. ## A first example Here is a script where only the last line is to blame, surrounded by setup that has nothing to do with the failure. ```{r} script <- c( "a <- 10", "b <- 20", "log('not a number')" ) res <- minex(code = script, backend = "inprocess") res ``` The setup lines are gone. `as.character()` gives you the bare code, ready to paste into a bug report. ```{r} cat(as.character(res), "\n") ``` (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.) ## Dependencies are respected 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. ```{r} script <- c( "x <- c(1, 2, NA)", "m <- mean(x)", "if (is.na(m)) stop('mean is NA')" ) minex(code = script, backend = "inprocess") ``` ## What counts as "the same failure" 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: ```{r} minex(code = script, match = "class", backend = "inprocess") ``` Use `match = "both"` to require the message and a shared class. ## Minimizing against an arbitrary condition 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. ```{r} minex( code = c("one <- 1", "two <- 2", "three <- 3"), oracle = function(stmts) any(grepl("two", stmts)), backend = "inprocess" ) ``` ## Reducing the data, not just the code 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. ```{r} df <- data.frame(id = 1:6, value = c(3, 8, 999, 2, 5, 7)) reduce_rows(df, function(d) any(d$value > 100)) ``` The result is usually small enough to capture with `dput()`. ## The algorithm 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. ```{r} ddmin(strsplit("the quick brown fox", " ")[[1]], function(s) "fox" %in% s) ``` 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. ## Limitations `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.