Information for package authors

Guidance for Package Authors

This section of the documentation describes how the external pointers to C++ function that are produced by ast2ast can be used in other packages. This information is intended for package authors who want to use the translated functions within their own code. Additionally, this section endeavors to present pertinent implementation details, providing valuable insights into the inner workings of the process.

The headers of ast2ast include RcppThread. Therefore, a package using them has to add both to its DESCRIPTION: LinkingTo: Rcpp, ast2ast, RcppThread. When using Rcpp::sourceCpp the corresponding attribute is // [[Rcpp::depends(ast2ast, RcppThread)]].

Compiling R files into a package: make_package

make_package translates R files into the C++ code of an existing package, like Rcpp::compileAttributes. The headers are copied into the package, so it does not depend on ast2ast afterwards.

Place the files outside of R/, e.g. in inst/tools/:

# inst/tools/types.R
new_type(Point, slots(x |> type(double), y |> type(double)))

# inst/tools/f.R
f <- function(p) {
  argtypes(p |> type(Point))
  returntype(double)
  s <- p$x + p$y
  helper(s)
}
helper <- function(s) {
  argtypes(s |> type(double))
  returntype(double)
  s * 2
}
ast2ast::make_package(c("inst/tools/types.R", "inst/tools/f.R"), "f")
  • Only f is exported; helper is internal. Several entry points: make_package(files, c("f", "g")), one C++ file each.
  • The files may only contain new_type(...) and function definitions.
  • The package needs Imports: Rcpp, LinkingTo: Rcpp, RcppThread and in the NAMESPACE useDynLib(<package>, .registration = TRUE), importFrom(Rcpp, evalCpp). make_package checks this and reports what is missing. Add export(f) if users should call f.
  • Rerun make_package after changing the R files.
  • Runtime errors name package, file, line and function, e.g. In 'mypkg: f.R:5 (f): ...'.

Naming rationalization ast2ast

The nomenclature of the package, ast2ast, is rooted in the abbreviation ast, signifying abstract syntax tree. Originally I planned to convert the abstract syntax tree of R to the C++ tree as the literature recommended for transpilers. However, through iterative refinement, a more optimal methodology emerged. The development incorporated an Expression Template Library in C++ known as ETR, meticulously crafted to mimic R. Thus, R code is translated into ETR code, which is subsequently compiled. The original ETR library is accessible at https://github.com/Konrad1991/ETR. It’s imperative to note that the version integrated into ast2ast has undergone substantial enhancements, amplifying its efficacy and adaptability.

Comparison of R and ETR code

Displayed below is a basic bubble sort function implemented in R on the left, juxtaposed with its ETR counterpart on the right. It is obvious that the two code snippets are quite similar. Remarkably, the overall structure of the R code remains unaltered. Instead, the substitution of individual functions with their ETR equivalents forms the crux of the transformation.
In the C++ code, all functions are located in the etr namespace. Certain functions share identical names in both R and C++, such as the length function. To mitigate potential conflicts, these calls are modified to explicitly reference the etr namespace, resulting in expressions like etr::length. Other functions as for example : and [ cannot be defined in C++ (at least not in the way they are used in R) therefore they are replaced by functions with new names e.g. etr::colon and etr::subset.
Additionally, C++ necessitates explicit declaration of variable types. The argument a keeps the vector type Array<Double, Buffer>, while size and temp are inferred as scalars (etr::Integer and etr::Double respectively), since length() and single-element subsetting both produce scalars. A detailed explanation is deferred to the subsequent section.

bubbleSort <- 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)
}
#include <Rcpp.h>
// [[Rcpp::depends(ast2ast, RcppThread)]]
// [[Rcpp::plugins(cpp2a)]]
#include "etr.hpp"
// [[Rcpp::export]]
SEXP bubbleSort(SEXP aSEXP) {
  etr::Array<etr::Double, etr::Buffer<etr::Double>> a = aSEXP;
  etr::Integer size;
  etr::Double temp;
  size = etr::length(a);
  for (const auto i : etr::colon(etr::Double(1.0), size)) {
    for (const auto j : etr::colon(etr::Double(1.0), (size - etr::Double(1.0)))) {
      if (etr::subset(a, j) > etr::subset(a, j + etr::Double(1.0))) {
        temp = etr::subset(a, j);
        etr::subset(a, j) = etr::subset(a, j + etr::Double(1.0));
        etr::subset(a, j + etr::Double(1.0)) = temp;
      }
    }
  }
  return(etr::Cast(a));
}

The XPtr interface

The XPtr interfaces creates an external pointer which can be used in other C++ programs.

First, one creates the external pointer of the R function.

f <- function(a, b) {
  argtypes(
    a |> type(vec(double)) |> ref(),
    b |> type(vec(double)) |> ref()
  )
  c <- a + b
  return(c)
}
fcpp <- ast2ast::translate(f, output = "XPtr")

Afterwards the external pointer is used in an R package

#include <Rcpp.h>
// [[Rcpp::depends(ast2ast, RcppThread)]]
// [[Rcpp::plugins(cpp2a)]]
#include "etr.hpp"

typedef etr::Array<etr::Double, etr::Buffer<etr::Double>> (*FP)(etr::Array<etr::Double, etr::Buffer<etr::Double>>& a, etr::Array<etr::Double, etr::Buffer<etr::Double>>& b);

// [[Rcpp::export]]
void call_xptr(Rcpp::XPtr<FP> ep) {
  FP f = *ep;
  etr::Array<etr::Double, etr::Buffer<etr::Double>> a;
  etr::Array<etr::Double, etr::Buffer<etr::Double>> b;
  etr::Array<etr::Double, etr::Buffer<etr::Double>> c;
a = etr::c(etr::Double(1), etr::Double(2), etr::Double(3));
b = etr::c(etr::Double(4), etr::Double(5), etr::Double(6));
  c = f(a, b);
  etr::print(c);
}

call the function from the R package

call_xptr(fcpp)
## 5    7   9