The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
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[@]dogado.de.

When does ast2ast duplicate your data, and when do writes inside a function stay visible to the caller? It depends on which boundary the argument crosses – and the same keyword can mean different things on each.

Base R never faces this question: vectors and lists are copy-on-modify, so a change a function makes to its argument never escapes. The one keyword that changes that – ref – has no counterpart in ordinary R, so it is worth being precise about where it applies.

Boundary 1 – R calls the translated function

The argument arrives as a SEXP (an ordinary R object) and is turned into an etr vector before the body runs.

default vec in R holds [9, 5, 6] SEXP data · 0x…d2b0 copy on call [9, 5, 6] fresh copy · 0x…22b0 holds x in f
Copied. The SEXP contents land in f’s own vector at a new address. Your R vector is unchanged after the call.
borrow_vec(double) vec in R points to one buffer, two names [9, 5, 6] SEXP data · 0x…d2b0 points to x in f
Shared. f’s vector points straight into R’s SEXP buffer – same address, no copy. Without const(), a write here reaches the caller’s storage.
scale_inplace <- function(v, k) {
  argtypes(
    v |> type(borrow_vec(double)),
    k |> type(double)
  )
  for (i in seq_len(length(v))) {
    v[[i]] <- v[[i]] * k
  }
  return(v)
}
f <- ast2ast::translate(scale_inplace)

x <- c(1, 2, 3)
f(x, 10)
x   # c(10, 20, 30): x itself was modified, because f borrowed its buffer

Drop the borrow_vec – plain v |> type(vec(double)) – and x stays c(1, 2, 3): the function worked on a copy.

Boundary 2 – inside f, calling an inner fn()

The argument is already an etr object, not a SEXP – so ref() works here whatever output you translate to. In this sense inner fn() arguments behave like XPtr arguments.

default vec in f holds [9, 5, 6] etr object · 0x…d2b0 copy on call [9, 5, 6] fresh copy · 0x…22b0 holds y in g
Copied. The inner function g gets its own copy of the etr object. Whatever g writes stays in g.
y |> type(vec(double)) |> ref() vec in f points to same object, two names [9, 5, 6] etr object · 0x…d2b0 points to y in g
Shared. g works on the caller’s object itself. After g returns, the changes are still there – the one place ast2ast behaves unlike base R.
demo <- function(v) {
  argtypes(v |> type(vec(double)))

  double_it <- fn(
    argtypes(out |> type(vec(double)) |> ref()),
    return(void),
    {
      for (i in seq_len(length(out))) {
        out[[i]] <- out[[i]] * 2
      }
    }
  )

  double_it(v)   # v is filled in place -- no copy, no allocation
  return(v)
}
f <- ast2ast::translate(demo)
f(c(1, 2, 3))    # c(2, 4, 6)

Without |> ref() on out, double_it would double a private copy and v would come back unchanged. This is the mechanism behind the “preallocate a buffer once, let an inner function fill it every iteration” pattern – it removes a per-call allocation from hot loops.

noteref() on an argument of the outer function is ignored, with a warning. Converting R’s SEXP into an etr object has already happened, so there is no shared handle left to pass by reference. Use borrow_vec() / borrow_mat() to skip the copy at that boundary.

In one table

Keyword Boundary Copies the data Writes visible to caller
(default) R → translated f yes no
borrow_vec() / borrow_mat() R → translated f no yes †
(default) f → inner fn() yes no
\|> ref() f → inner fn() no yes
\|> ref() R → translated f -- ignored, warning --

† unless the argument is also marked const().

The short version

borrow = don’t copy the data. ref = don’t copy the object.

The observable rule, without pointers or SEXP in it: a copied argument’s changes stop at the function; a shared argument’s changes are still there after it returns. The addresses in the figures are shortened from a real run of vec <- c(9, 5, 6) – 0x...d2b0 is its storage, 0x...22b0 a fresh allocation.

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.
Health stats visible at Monitor.