---
title: "Using Rust inside targets"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Using Rust inside targets}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE)
```

This vignette covers running **Rust** as `targets` steps with [rextendr](https://extendr.rs/rextendr/) / [extendr](https://extendr.rs/). A Rust step is deliberately different from a Python or Julia one: there is **no live interpreter**. Instead the `#[extendr]` functions in your script are *compiled* into a dynamic library and exposed as ordinary R functions, which an R **post-script** then calls. For a gentler tour start with `vignette("get_started")`.

Code blocks are illustrative and **not executed** when the vignette builds (compiling Rust needs a toolchain).

## Do you need `tar_target_rs()`?

Often not, and it is worth being honest about when it earns its keep. Unlike Python and Julia, a Rust step has no live interpreter and no data hand-off to abstract away: `rextendr::rust_source()` compiles your `#[extendr]` functions and hands them back as ordinary R functions. For the simple case you can call it directly inside a plain `tar_target()`, keeping every native `targets` convenience (automatic dependency detection, inline R, a single file):

```r
library(targets)

list(
  tar_target(petal_length, iris$Petal.Length),

  tar_target(petal_z, {
    rextendr::rust_source("rs/scale.rs")   # compiles zscore() and puts it in scope
    zscore(petal_length)                   # petal_length is auto-detected as a dependency
  })
)
```

This is the recommended starting point when your Rust toolchain is already discoverable, which is the usual situation for local and interactive runs. `tar_target_rs()` is a convenience wrapper that becomes worth it when you want one or more of the following:

* **Robust builds in bare or `crew` worker processes.** `tar_target_rs()` puts R, `cargo`, and (on Windows) Rtools on `PATH` and sets `R_HOME` for the compilation; a plain `tar_target()` does not, so a Rust build in a fresh worker often fails to find the toolchain unless you arrange that yourself.
* **Per-step toolchain selection** through `toolchain =`, which sets `RUSTUP_TOOLCHAIN` for that build only.
* **File output, script tracking, and the full `tar_target_raw()` argument set** bundled in one call (`output = "file"`, `tar_target_path()`, `pattern`, `resources`, `cue`, and so on).
* **API symmetry** with `tar_target_py()` and `tar_target_jl()`, so a polyglot pipeline reads the same way across all three languages.

If none of those apply, the plain `tar_target()` above is simpler and gives up nothing. The rest of this vignette documents `tar_target_rs()` for when they do.

## The two constructors

* `tar_target_rs()`: bare `name` (and unquoted `pattern`), for direct use in `_targets.R`.
* `tar_target_rs_raw()`: string `name`, for use inside targets factories.

Both return a single `targets` target and forward every `targets::tar_target_raw()` argument.

## How a Rust step differs

* **No pre-script.** There is no foreign session to push variables into. The `inputs` are bound directly in the **post-script**, where you call the compiled functions with them.
* **The post-script is where the work happens.** After compilation, the Rust functions are in scope in the post-script's environment (alongside `inputs`). Its last expression is the target value (object mode); it returns file paths (file mode).
* **Real type conversion** (via extendr), not JSON, but return values still have to be R-representable, or use `output = "file"`.

## Arguments

| Argument | Meaning |
|---|---|
| `script` | Path to the Rust file with `#[extendr]` functions (**required**). |
| `post_script` | R script run after compilation; the compiled functions and `inputs` are in scope. Required for object mode. |
| `inputs` | Named vector mapping in-step names to upstream targets, e.g. `c(x = "value")`. |
| `output` | `"object"` (default) or `"file"`. |
| `files` | Paths to return when there is no post-script (file mode). |
| `dependencies` | Crate dependencies as a named list, passed to `rextendr::rust_source()`. |
| `features` | Cargo features, passed to `rextendr::rust_source()`. |
| `profile` | Build profile (e.g. `"dev"` or `"release"`). |
| `toolchain` | Optional rustup toolchain (e.g. `"stable-x86_64-pc-windows-gnu"`); sets `RUSTUP_TOOLCHAIN` for the build. |

As with the other languages, `script` and `post_script` may be a literal path or a `tar_target_path("name")` reference to track the file.

## Installing Rust and rextendr

1. Install the Rust toolchain with [rustup](https://rust-lang.org/tools/install/). **On Windows, use the GNU toolchain** so it matches R's mingw ABI:

    ```sh
    rustup toolchain install stable-x86_64-pc-windows-gnu
    rustup default stable-x86_64-pc-windows-gnu
    ```

2. Install rextendr and check it works:

    ```r
    install.packages("rextendr")
    rextendr::rust_source(code = "#[extendr] fn double(x: f64) -> f64 { x * 2.0 }")
    double(21)   # 42
    ```

`run_rs_step()` puts R, `cargo`, and (on Windows) Rtools on `PATH` for the build itself, so a Rust step usually works even in a bare `crew` worker, but the toolchain must be installed.

## Object output (iris example)

`rs/scale.rs`: one `#[extendr]` function:

```rust
#[extendr]
fn zscore(x: Vec<f64>) -> Vec<f64> {
    let n = x.len() as f64;
    let mean = x.iter().sum::<f64>() / n;
    let sd = (x.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n).sqrt();
    x.iter().map(|v| (v - mean) / sd).collect()
}
```

`R/scale_post.R`: the compiled `zscore()` and the input `petal` are in scope:

```r
# `petal` is bound from inputs = c(petal = "petal_length").
scaled <- zscore(petal)
data.frame(petal = petal, z = scaled)   # last expression = target value
```

`_targets.R`:

```r
library(targets)
library(tarpolyglot)

list(
  tar_target(petal_length, iris$Petal.Length),

  tar_target_rs(
    name = petal_z,
    script = "rs/scale.rs",
    inputs = c(petal = "petal_length"),
    post_script = "R/scale_post.R"
  )
)
```

## File output

```r
tar_target_rs(
  name = rs_file,
  script = "rs/write.rs",            # a fn that writes a file and returns its path
  inputs = c(x = "value"),
  post_script = "R/rs_files_post.R", # returns the path(s)
  output = "file"
)
```

## Crate dependencies, features, profile

Pass extra crates through `dependencies` (a named list), and build options through `features` / `profile`:

```r
tar_target_rs(
  name = parsed,
  script = "rs/parse.rs",            # uses serde_json
  inputs = c(txt = "raw_json"),
  post_script = "R/parse_post.R",
  dependencies = list(`serde_json` = "1"),
  profile = "release"
)
```

## Dynamic branching

Like the other constructors, `inputs` are real dependencies, so `pattern` branches them (and `crew` spreads the branches):

```r
list(
  tar_target(groups, split(iris$Petal.Length, iris$Species), iteration = "list"),
  tar_target_rs(
    name = z_by_group,
    script = "rs/scale.rs",
    inputs = c(petal = "groups"),
    post_script = "R/scale_post.R",
    pattern = map(groups),
    iteration = "list"
  )
)
```

Plain `map(groups)` here recompiles the crate in **every** branch: a Rust step has no live interpreter to reuse, so each branch runs `cargo` from scratch. With many branches that repeated compilation is the dominant cost.

## Compile the Rust code only once across branches

`tarpolyglot` provides dynamic-branching pattern helpers that mirror the `targets` patterns but compile the Rust crate a **single time** and reuse it across all branches. Each is named after its `targets` equivalent:

| tarpolyglot helper | `targets` pattern |
|---|---|
| `tarpolyglot_map()` | `map()` |
| `tarpolyglot_cross()` | `cross()` |
| `tarpolyglot_slice()` | `slice()` |
| `tarpolyglot_head()` | `head()` |
| `tarpolyglot_tail()` | `tail()` |
| `tarpolyglot_sample()` | `sample()` |

Use one exactly where you would use the plain pattern:

```r
list(
  tar_target(groups, split(iris$Petal.Length, iris$Species), iteration = "list"),
  tar_target_rs(
    name = z_by_group,
    script = "rs/scale.rs",
    inputs = c(petal = "groups"),
    post_script = "R/scale_post.R",
    pattern = tarpolyglot_map(groups),   # was map(groups): now compiles once
    iteration = "list"
  )
)
```

On a Rust step, `tarpolyglot_map()` expands that single constructor call into **two** targets:

* `z_by_group_rust_lib`: compiles the crate once (this target is not branched). Its value is the compiled library, embedded so any `crew` worker can reload it without a toolchain.
* `z_by_group`: branches over `groups` as before, but each branch reloads the pre-compiled library (a `dyn.load()`, in milliseconds) instead of recompiling.

You still write one `tar_target_rs()`; the companion `<name>_rust_lib` target appears automatically in the pipeline (visible in `tar_visnetwork()`), and `targets` recompiles it only when the Rust source changes. So `pattern = tarpolyglot_map(x)` turns `N x compile` into `1 x compile` plus `N` near-instant reloads.

The other pattern helpers behave the same way, differing only in which branches `targets` builds (`tarpolyglot_cross()` for all combinations of several inputs, `tarpolyglot_head()` / `tarpolyglot_tail()` / `tarpolyglot_slice()` / `tarpolyglot_sample()` for a subset). The plain `targets` patterns (`map()`, `cross()`, and so on) keep working unchanged; the `tarpolyglot_*` helpers are simply the opt-in that adds compile-once on Rust.

### They fall back to the plain pattern on Python and Julia

The same helpers are accepted by `tar_target_py()` and `tar_target_jl()`, where they are rewritten to the plain `targets` pattern: Python and Julia reuse a live interpreter, so there is nothing to compile and nothing to reuse. `tarpolyglot_map()` on a Python step is therefore exactly `map()`. This lets one pipeline branch every language with the same helper:

```r
list(
  tar_target(vals, c(10, 20, 30)),
  # Python: behaves exactly as map(vals).
  tar_target_py(
    name = py_b, script = "py/sq.py", inputs = c(x = "vals"),
    pre_script = "R/push.R", retrieve = "result",
    pattern = tarpolyglot_map(vals)
  ),
  # Rust: compiles once in py_b's sibling rs_b_rust_lib, reuses across branches.
  tar_target_rs(
    name = rs_b, script = "rs/square.rs", inputs = c(x = "vals"),
    post_script = "R/post.R",
    pattern = tarpolyglot_map(vals)
  )
)
```

The `tarpolyglot_*` helpers are recognised only inside the tarpolyglot constructors. A plain `targets::tar_target()` does not know them, so there you use the native `map()` / `cross()` / ... directly (a plain step has no foreign code to compile anyway).

## Windows toolchain note

rextendr requires the Rust **GNU** toolchain on Windows (to match R's mingw ABI). Either set it as the rustup default (shown above) or pass it per step:

```r
tar_target_rs(
  name = petal_z,
  script = "rs/scale.rs",
  inputs = c(petal = "petal_length"),
  post_script = "R/scale_post.R",
  toolchain = "stable-x86_64-pc-windows-gnu"   # sets RUSTUP_TOOLCHAIN for the build
)
```

## Trade-offs vs Python/Julia

Because a Rust step compiles rather than using a live interpreter:

* **No interpreter-per-session or global-state issues.** Each step compiles and loads its own library, so different steps can freely use different crates/toolchains in the same session.
* **Compilation cost instead of interpreter start-up.** The first build compiles the extendr crate and your code (tens of seconds); cargo caches artefacts, so re-runs are faster.

See `?tar_target_rs` and `?run_rs_step` for the full argument reference, and `vignette("get_started")` for how Rust steps fit alongside Python/Julia and `crew`.
