The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.
The ast2ast package translates R functions into C++ functions, returning either an external pointer (XPtr) or an R function. This package is particularly useful for tasks requiring frequent function evaluations, such as solving ODE systems or optimization problems. Using the external pointer generated by C++ can significantly enhance performance, as shown in the benchmark below.
Supported objects:
new_type belowSupported functions:
=, <-, and $
(struct field access/assignment)vector, logical,
integer, numeric, :,
c, rep, matrix, and
arraylength, dim,
nrow, ncol, is.na,
is.nan, is.finite, is.infinite,
print, and stop+, -, *,
/, %%, %/%, %*%,
sin, asin, sinh,
cos, acos, cosh,
tan, atan, tanh,
log, sqrt, abs,
sign, ^, exp, floor,
ceiling, trunc, and roundsum, prod, mean,
max, min, which.max,
which.min, which, all,
anyrev, sort,
cumsumcolSums, rowSums,
colMeans, rowMeansifelset, chol,
crossprod, tcrossprod, diag,
get_diag, solve, backsolve,
forwardsolve, rbind, cbinduniroot,
nnlsfn): map,
Reduce, Filter, applyfn: jacobian,
lbfgsb, psoas.numeric, as.integer,
as.logicalseq_len, seq_along[], [[]], and
at() (alias for [[]])for, while,
repeat, if, else if, and
else==, !=, >,
<, >=, and <=&&, ||,
&, |, !seed, unseed,
get_dot, derivfn, see belowR is dynamically typed, while C++ is statically typed. When translating an R function, ast2ast must decide static C++ types for:
f, andf.In ast2ast, every type is a combination of:
logical,
integer (or int), doublescalar,
vector / vec, matrix /
matTypical types are therefore:
double (scalar double)vec(double) / vector(double) (double
vector)mat(double) / matrix(double) (double
matrix)A key difference to R: ast2ast scalars are true
scalars, not length-1 vectors. Scalars cannot be subset using
[] or [[ ]].
Another difference to R: negative indices are not
supported (R’s v[-1L] drop-element form). Indices
must be positive and 1-based.
Another difference to R: variables are declared and zero-initialised up front (Fortran-style: all declarations first, then the body). A variable that is only assigned on one branch but read unconditionally therefore does not raise an “object not found” error the way R would – it reads its zero value. ast2ast does not currently perform use-before-assignment analysis, so make sure every path that reads a variable also assigns it.
f <- function(a) {
if (a > 0) x <- 1.0 # x is only assigned here...
return(x) # ...but read here regardless: returns 0.0 when a <= 0
}
If no argtypes(...) block is present, all arguments
default to matrix(double), as this is most convinient for
numeric code.
argtypes()To control argument types, make argtypes(...) the
first statement of f’s body. Each entry
names one of f’s arguments and assigns its type with
type():
f <- function(a, b, c) {
argtypes(
a |> type(vec(double)),
b |> type(mat(double)),
c |> type(double)
)
# ... body ...
}
f_cpp <- ast2ast::translate(f)
For arguments, you can additionally control how values are passed:
borrow_vec(...), borrow_mat(...): borrow
memory (no copy)const(): disallow modificationref(): pass by reference (only valid when
output = "XPtr")Example:
f <- function(a, b, c) {
argtypes(
a |> type(borrow_vec(double)) |> ref(), # mutable, passed by reference
b |> type(borrow_mat(double)) |> ref() |> const(), # read-only matrix reference
c |> type(double) |> ref() # scalar reference (XPtr only)
)
# ... body ...
}
Notes:
const() is enforced by the static checker at
translation time (“You cannot assign to a constant variable”), before
any C++ compilation happens.ref() is primarily intended for the external pointer
interface.Types inside f are often inferred automatically
from:
numeric(), matrix(),
integer(), etc.)You can override inference using explicit annotations.
In R, reassigning a variable to a different type mid-function is completely normal:
a <- 1L
a <- 2.5
The generated C++ can’t do that. A variable has exactly
one type for its entire lifetime, declared once, before
any of the function’s logic runs. So when a variable’s type isn’t pinned
down with an explicit type() annotation, ast2ast has to
decide that one type – and it does so by looking at
every assignment to that variable across the whole
function, not just the first one, and picking the type wide enough to
hold all of them. That decision is then applied retroactively to
all of the variable’s assignments – including ones that come
before the line that actually forced the wider type:
f <- function() {
a <- 1L # looks like an integer assignment...
a <- 2.5 # ...but a is reassigned a double two lines later
return(a)
}
Because a is also assigned a double further
down, ast2ast decides a is double for the
whole function. The generated C++ declares a as
double up front, and even the first assignment
(a <- 1L) compiles to storing 1.0 – not
1L. A warning is raised whenever this happens
(Promoted the type of variable a from ... to ...), so it’s
visible rather than silent.
This “pick the wider of two types” logic is the same one used to
combine two operands within a single expression
(e.g. integer + double), along two independent precedence
orders:
logical < integer
(int) < doublescalar < vector
(vec) < matrix (mat) <
arrayThe result takes the higher-precedence base type and the higher-precedence structure of the two operands (or, for a variable, of everything ever assigned to it), e.g.:
logical + integer -> integerinteger + double -> doubledouble (scalar) + vec(double) ->
vec(double)It only ever widens – never narrows, and never reconciles genuinely incompatible kinds (e.g. a custom type and a plain vector; those are hard errors, not promotions).
A few operators promote unconditionally, regardless of what they’re given:
/ and ^ always produce double
(R never returns an integer from division or exponentiation)sin, sqrt,
log, exp, …) always return
doublesum() keeps double as double,
but promotes logical/integer to
integer (matches R’s own sum() behavior)This promotion mechanism only applies to inferred
variables. A variable whose type was fixed explicitly – via a
type() annotation, or as a function argument – is truly
immutable: it does not widen, and assigning it something outside that
fixed type is a translation-time error, not a promotion. If you need a
genuinely different type or structure somewhere in the function, create
a new variable with a new name rather than relying on an existing one to
change shape.
The ast2ast package provides built-in support for automatic differentiation (AD) in both forward mode and reverse mode. Derivative support is enabled when translating a function via the derivative argument:
fcpp <- ast2ast::translate(f, derivative = "forward")
fcpp <- ast2ast::translate(f, derivative = "reverse")
Unlike many high-level AD frameworks, ast2ast intentionally exposes a
low-level and explicit interface. Derivative computations are assembled
from a small set of primitive operations (seed,
unseed, get_dot, deriv), which
keeps the behavior transparent, predictable, and close to the generated
C++ code. A built-in jacobian(f, x) wrapper is also
available for the common case (see Optimizers below); it is
implemented on top of the same primitives.
In forward mode, derivatives are propagated alongside values. Internally, each scalar carries both its value and its directional derivative (also called its dot value). The following functions are available:
A typical pattern is to compute Jacobians column-by-column by looping over the input variables:
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")
Forward mode is most efficient when the number of inputs is small relative to the number of outputs.
In reverse mode, derivatives are accumulated by propagating sensitivities backward from the outputs to the inputs. This is particularly efficient when the number of outputs is small relative to the number of inputs.
Reverse mode provides the function:
Example:
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")
The call to deriv() must appear explicitly in your function body. No automatic differentiation is performed unless requested.
Derivative computation in ast2ast is explicit by design. The full control flow—loops, seeding, unseeding, derivative extraction, and accumulation—is written directly in R and translated into C++.
This approach: * avoids hidden performance costs, * makes derivative logic easy to inspect and debug, * gives full control over memory and evaluation order, * maps naturally to high-performance C++ code.
Rather than hiding differentiation behind abstractions, ast2ast treats derivatives as first-class values that can be manipulated like any other object.
Functions can be defined inside f using
fn(). This is required whenever a function needs to be
passed as a value, e.g. to uniroot(). An inner function is
declared with three positional parts: argtypes(...) (types
of its arguments, same form as the outer argtypes(...)
block), return(...) (its return type), and a
{ } block (the R code). A single-statement body does not
need the { }, but wrapping it is fine too.
f <- function(a) {
argtypes(a |> type(int))
factorial <- fn(
argtypes(a |> type(int) |> const()),
return(int),
{
if (a == 1L) return(a) else return(a * factorial(a - 1L))
}
)
return(factorial(a))
}
fcpp <- ast2ast::translate(f)
An inner function’s non-const parameters only bind to
bare variables passed at the call site, not to arbitrary expressions
(e.g. x + 1, x[[1L]]) – a
non-const parameter is a mutable reference to the caller’s
argument, and an expression has no addressable storage to reference.
Declare the parameter const() if you need to pass an
expression:
sq <- fn(
argtypes(x |> type(double) |> const()), # const -- accepts expressions
return(double),
return(x * x)
)
# sq(a + b) is fine; without const() on x, only sq(a) (a bare variable) would be.
Inner functions can call each other (including mutual recursion) and
can be passed to functions expecting a function argument,
e.g. uniroot:
f <- function(interval) {
argtypes(interval |> type(vec(double)))
g <- fn(
argtypes(x |> type(double)),
return(double),
{
return(x^2 - 4)
}
)
res <- uniroot(g, interval, 1e-10, 1000)
return(res$root)
}
fcpp <- ast2ast::translate(f)
uniroot(f, interval, tol, maxiter) returns a struct with
fields root, f_root, iter, and
estim_prec (accessed via $, see custom types
below). f must take a single double and return
a double; an optional fifth argument is passed through to
f as extra data (f then takes two
arguments).
nnls(A, b) solves the non-negative least squares problem
and returns the solution vector directly.
These take an inner function (fn) as their first
argument.
map(f, x, ...) – apply f element-wise over
the given vectors; scalars broadcast. The result shape follows
f’s return type: scalar produces a vector, vector a matrix,
matrix/array an array (with n as the last axis), and a
new_type a collection.Reduce(f, x) – left fold seeded with
x[[1]]; f is
fn(acc, elem) -> acc and must return the same type as
its accumulator argument.Filter(f, x) – keep the elements of vector
x for which f(elem) is TRUE
(f returns a logical scalar).apply(f, MARGIN, x) – function first, unlike base R’s
apply(X, MARGIN, FUN). x is a matrix,
MARGIN is 1 (rows) or 2
(columns), and f maps a vector to a scalar or a
vector.f <- function(x) {
argtypes(
x |> type(vec(double))
)
sq <- fn(
argtypes(
a |> type(double) |> const()
),
return(double),
return(a * a)
)
return(map(sq, x))
}
fcpp <- ast2ast::translate(f)
fcpp(1:5)
jacobian(f, x[, data]) – the
m-by-n Jacobian of f (double
vector to double vector) evaluated at x. Requires
derivative = "forward" or "reverse".lbfgsb(f, x, lower, upper, maxit, factr, pgtol, lmm[, data])
– bound-constrained L-BFGS-B via R’s own C routine. f maps
a double vector to a scalar double. Under
derivative = "forward" / "reverse" the
gradient is exact (computed with jacobian); otherwise it is
a central-difference approximation. Returns a struct with
$par, $value, $convergence,
$counts.pso(f, lower, upper, ngen, npop, error_threshold, global[, data])
– derivative-free particle-swarm optimisation. f maps a
double vector to a scalar double; returns the best parameter vector
found.For lbfgsb and pso the optional trailing
data argument (any non-function, non-character value) is
passed to f unchanged as a second argument, so
f then takes two arguments.
rosen <- function(p) {
argtypes(
p |> type(vec(double))
)
loss <- fn(
argtypes(
x |> type(vec(double)) |> const()
),
return(double),
{
a <- 1.0 - x[[1L]]
b <- x[[2L]] - x[[1L]] * x[[1L]]
return(a * a + 100.0 * b * b)
}
)
lo <- c(-5.0, -5.0)
up <- c(5.0, 5.0)
res <- lbfgsb(loss, p, lo, up, 100L, 1e7, 1e-8, 5L)
return(res$par)
}
fcpp <- ast2ast::translate(rosen, derivative = "reverse")
fcpp(c(-1.2, 1.0))
new_type)Besides scalars, vectors, matrices and arrays,
ast2ast supports user-defined struct types. They are
declared in a types_f helper function passed to
translate():
types_f <- function() {
new_type(Point, slots(x |> type(double), y |> type(double)))
}
f <- function(p) {
argtypes(p |> type(Point))
p$x <- p$x + 1
return(p)
}
fcpp <- ast2ast::translate(f, types_f = types_f)
p <- structure(list(x = 1, y = 2), class = "Point")
fcpp(p)
Notes:
list
with a matching class attribute,
e.g. structure(list(x = 1, y = 2), class = "Point").collection(TypeName) is a vector of a custom type, e.g.
slots(points |> type(collection(Point))) or, as a local
variable, coll |> type(collection(Point)). Allocate one
with vector(mode = "Point", n).$, including chained
access such as s$circles[[1L]]$center$x.To interpolate values, the ‘cmr’ function can be used. The function needs three arguments.
f <- function() {
dep <- c(0, 1, 0.5, 2.5, 3.5, 4.5, 4)
indep <- 1:7
evalpoints <- c(
0.5, 1, 1.5, 2, 2.5,
3, 3.5, 4, 4.5, 5,
5.5, 6, 6.5
)
for (i in evalpoints) {
print(cmr(i, indep, dep))
}
}
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.