translateR Documentation

Translate an R function into C++

Description

translate() compiles an R function into C++ code using the ast2ast expression template library. The result can be called directly from R (default) or returned as an external pointer. The C++ source code can also be retrieved without compilation.

Usage

translate(
  f,
  args_f = NULL,
  types_f = NULL,
  output = "R",
  derivative = NULL,
  verbose = FALSE,
  getsource = FALSE,
  debug = TRUE
)

Arguments

f

An R function to be translated into C++.

args_f

A helper function that defines the types of the arguments of f. Optional; defaults to NULL. See the Details section.

types_f

A helper function that declares custom struct types via new_type(), for use as argument types, variable types, or slot types inside f. Optional; defaults to NULL. See the Details section.

output

Controls what is returned:

  • "R" (default): an R function wrapping the compiled C++.

  • "XPtr": an external pointer to the compiled C++ function.

derivative

When derivatives are required one can set here the mode of the automatic differentiation:

  • NULL (default): You don't require any derivatives.

  • "forward" Determine the derivatives via forward automatic differentiation.

  • "reverse" Determine the derivatives via reverse automatic differentiation.

verbose

Logical. If TRUE, prints output from the compilation process.

getsource

Logical. If TRUE, returns the generated C++ source code as a string instead of compiling it.

debug

Logical. Defaults to TRUE. When TRUE, a runtime error is reported as "In '<statement>': <message>", naming the specific line of translated code that failed. Set to FALSE to get the plain, unattributed message instead.

Details

Supported functions

The following R constructs are currently supported:

  1. Assignment: =, <-, and $ (struct field access/assignment)

  2. Allocation: vector, matrix, array, rep, logical, integer, numeric

  3. Object info: length, dim, nrow, ncol

  4. Arithmetic: +, -, *, /, %%, %/%, %*%, ^

  5. Indexing: [] and [[]]

  6. Math: sin, asin, sinh, cos, acos, cosh, tan, atan, tanh, sqrt, abs, log, exp, floor, ceiling, trunc

  7. Reductions: sum, prod, max, min, which.max, which.min, all, any, rev

  8. Linear algebra: t, chol, crossprod, tcrossprod, diag, get_diag, solve, backsolve, forwardsolve, rbind, cbind

  9. Root finding / least squares: uniroot, nnls

  10. Casts: as.numeric, as.integer, as.logical

  11. Sequences: seq_len, seq_along

  12. Concatenation: c

  13. Control flow: for, while, repeat, next, break, if, else if, else

  14. Comparison: ==, !=, >, <, >=, <=

  15. Logical ops: &&, ||, &, |, !

  16. Printing: print

  17. Errors: stop

  18. Return: return

  19. Catmull–Rom spline: cmr

  20. Sequence operator: :

  21. Helpers: is.na, is.nan, is.finite, is.infinite

  22. Explicit typing: type()

  23. Derivative functions in forward mode: seed, unseed, and get_dot

  24. Derivative functions in reverse mode: deriv

  25. Inner functions: fn(), see below

  26. Custom types: new_type(), see below

Types are static in C++ and cannot be changed within the function. Each type consists of a base data type and a data structure:

Types are usually inferred automatically. Users may annotate explicitly, for example:

      a |> type(logical)      # scalar logical
      b |> type(vec(int))     # integer vector
      c |> type(mat(double))  # double matrix
    

Scalars in ast2ast differ from R: in R, scalars are length-1 vectors, but in C++ they are true scalars and cannot be subset.

Argument types

Arguments default to matrix(double) if args_f is NULL. To override, supply a function that annotates each argument:

      f_args <- function(a, b, c) {
        a |> type(borrow_vec(double)) |> ref()
        b |> type(borrow_mat(double)) |> ref() |> const()
        c |> type(double) |> ref()
      }
    

Supported extensions for arguments:

Inner functions

Functions can be defined inside f with fn(), which takes args_f (argument types, same form as the top-level args_f), return_value (the return type via type()), and block (the function body; does not need to be wrapped in { } for a single statement, but wrapping it is fine too). Inner functions may call each other, including recursively, and can be passed as values to functions expecting one, such as uniroot(). A non-const() parameter only binds to a bare variable at the call site, not to an arbitrary expression (e.g. x + 1); declare the parameter const() to accept expressions.

      factorial <- fn(
        args_f = function(a) a |> type(int) |> const(),
        return_value = type(int),
        block = function(a) {
          if (a == 1L) return(a) else return(a * factorial(a - 1L))
        }
      )
    

uniroot(f, interval, tol, maxiter) takes a function of a single double returning a double (or two arguments if a fifth, extra-data argument is supplied to uniroot), and returns a struct with fields root, f_root, iter, and estim_prec. nnls(A, b) solves the non-negative least squares problem and returns the solution vector.

Custom types

User-defined struct types are declared in a types_f helper function (passed to translate() via types_f =) and created with new_type() and slots():

      types_f <- function() {
        new_type(Point, slots(x |> type(double), y |> type(double)))
      }
    

On the R side, a value of a custom type is a named list with a matching class attribute, e.g. structure(list(x = 1, y = 2), class = "Point"). Fields are read and written with $, including chained access such as s$circles[[1L]]$center$x. A slot may itself be a custom type (nested structs) or collection(TypeName), a vector of a custom type; allocate one locally with vector(mode = "TypeName", n).

Derivatives

ast2ast supports automatic differentiation (AD) in two modes: forward and reverse. The mode is selected via the derivative argument of translate().

The AD system is intentionally low-level and explicit. Rather than providing high-level wrappers (such as a built-in jacobian()), users are expected to assemble derivative computations themselves using a small set of primitive building blocks. This keeps the interface flexible, transparent, and close to the generated C++ code.

Forward mode

In forward mode, derivatives are propagated alongside values. The following functions are available:

A typical pattern is to loop over input dimensions, seed one component at a time, evaluate the function, extract derivatives using get_dot(), and assemble the Jacobian manually.

Reverse mode

In reverse mode, derivatives are accumulated by backpropagation from outputs to inputs. The function

computes the Jacobian of y with respect to x. This call must appear explicitly in the translated function. Reverse mode is particularly efficient when the number of outputs is small relative to the number of inputs.

Design philosophy

Derivative computation in ast2ast is explicit by design. The full control flow (including loops, seeding, unseeding, and accumulation) is visible in the user code and translated directly into C++. This makes the generated code easy to inspect, reason about, and modify, and avoids hidden performance costs. Important: The derivative argument only enables the AD infrastructure. It does not automatically differentiate your function. You must explicitly call seed(), unseed(), get_dot(), or deriv() inside your function, depending on the chosen mode.

Note: The generated C++ mimics R semantics closely but not exactly. Always validate compiled functions against the original R implementation before using in production. See the vignette Detailed Documentation for a full comparison, and InformationForPackageAuthors for internals.

Value

Depending on output:

Examples

## Not run: 
# Hello World
# ----------------------------------------------------------------
f <- function() {
  print("Hello World!")
}
f_cpp <- ast2ast::translate(f)
f_cpp()

# Derivatives
# ----------------------------------------------------------------
f <- function(y, x) {
  y[[1L]] <- x[[1L]] * x[[2L]]
  y[[2L]] <- x[[1L]] + x[[2L]]*x[[2L]]
  jac <- deriv(y, x)
  return(jac)
}
fcpp_reverse <- ast2ast::translate(f, derivative = "reverse")
y <- c(0, 0)
x <- c(2, 3)
fcpp_reverse(y, x)

f <- function(y, x) {
  jac <- matrix(0.0, length(y), length(x))
  for (i in 1L:length(x)) {
    seed(x, i)
    y[[1L]] <- x[[1L]] * x[[2L]]
    y[[2L]] <- x[[1L]] + x[[2L]]*x[[2L]]
    d <- get_dot(y)
    jac[TRUE, i] <- d
    unseed(x, i)
  }
  return(jac)
}
fcpp_forward <- ast2ast::translate(f, derivative = "forward")
fcpp_forward(y, x)

# Bubble sort (using [[ for scalars)
# ----------------------------------------------------------------
bubble <- function(a) {
  size <- length(a)
  for (i in 1:size) {
    for (j in 1:(size - 1)) {
      if (a[[j]] > a[[j + 1]]) {
        temp <- a[[j]]
        a[[j]] <- a[[j + 1]]
        a[[j + 1]] <- temp
      }
    }
  }
  return(a)
}
bubble_cpp <- ast2ast::translate(bubble)
bubble_cpp(runif(10))

# Fibonacci sequence
# ----------------------------------------------------------------
fib <- function(n = 10) {
  f <- integer(n)
  f[[1L]] <- 1L
  f[[2L]] <- 1L
  for (i in 3L:n) {
    f[i] <- f[i-1L] + f[i-2L]
  }
  return(f)
}
fib_cpp <- ast2ast::translate(fib)
fib_cpp(10)

# Custom types
# ----------------------------------------------------------------
types_f <- function() {
  new_type(Point, slots(x |> type(double), y |> type(double)))
}
f <- function(p) {
  p$x <- p$x + 1
  return(p)
}
fcpp <- ast2ast::translate(f, args_f = function(p) p |> type(Point), types_f = types_f)
p <- structure(list(x = 1, y = 2), class = "Point")
fcpp(p)

# Inner functions and uniroot
# ----------------------------------------------------------------
f <- function(interval) {
  g <- fn(
    args_f = function(x) x |> type(double),
    return_value = type(double),
    block = function(x) {
      return(x^2 - 4)
    }
  )
  res <- uniroot(g, interval, 1e-10, 1000)
  return(res$root)
}
fcpp <- ast2ast::translate(f, args_f = function(interval) interval |> type(vec(double)))
fcpp(c(0, 10))

# External pointer interface
# ----------------------------------------------------------------
f <- function() {
  print("Hello World from C++")
}
ptr <- ast2ast::translate(f, output = "XPtr")

# Call from C++ side
Rcpp::sourceCpp(code = "
  #include <Rcpp.h>
  typedef void (*fp)();
  // [[Rcpp::export]]
  void call_fct(Rcpp::XPtr<fp> inp) {
    fp f = *inp;
    f();
  }")
call_fct(ptr)


## End(Not run)