The Runtime Inside Every Translated Program

Songyue Chen

2026-10-21

What a bundle contains

A run of sas_translate() has an offline START_HERE.html, reports, saved outputs, diagnostics, and an editable bundle/:

bundle/
  README.md
  run.R
  autoexec.R
  programs/              <- selected programs and included modules
  macros/                <- called macros as R functions
  runtime/
    sas2r-helpers.R
    _sas2r_formats.R
  output/                <- manual output only, created when needed

All available selected code is included, even if it failed checks or execution. Missing code is identified in the run manifest. The bundle runs without sas2r installed: autoexec.R loads its own runtime, formats, and macro definitions. Install the R packages listed in its README and configure input paths there. The runtime is generated from the same R/runtime-*.R source as the package.

Running a program

Export the bundle with sas_write(), then run Rscript run.R from that folder. The examples below copy an existing export into a temporary directory before running it. Set exported_bundle to the bundle you want to inspect:

exported_bundle <- "run_20260907T120000Z_1a2b3c4d"
bundle <- tempfile("sas2r-runtime-example-")
dir.create(bundle)
file.copy(list.files(exported_bundle, full.names = TRUE, all.files = TRUE,
                     no.. = TRUE), bundle, recursive = TRUE)
source(file.path(bundle, "run.R"), local = new.env(), chdir = TRUE)

To run selected programs yourself, load the configuration once and run required upstream programs before their consumers:

runtime <- new.env()
source(file.path(bundle, "autoexec.R"), local = runtime, chdir = TRUE)
source(file.path(bundle, "programs", "adsl.R"), local = runtime, chdir = TRUE)

Manual dataset writes go to output/datasets/<library>/, WORK writes to output/work/, and the launcher runs relative report writers in output/tlf/. These writes do not change saved automated outputs or reports. Set a fresh .sas2r_output_root when retaining separate manual iterations or testing supplied upstream data: generated members take precedence over input members. Other relative file access and explicit script paths may need human adjustment.

The generated bundle README contains concrete dependency-order commands and instructions for moving the entire bundle, copying or configuring data, and checking edited results. Loading a macro defines a function; it does not run the macro as a separate program.

Maintaining autoexec.R

Open it when data moves. The LIBRARIES section defines a list like the one below. local() keeps this example separate from the loaded runtime:

local({
  .sas2r_output_root <- runtime$.sas2r_output_root
  .sas2r_registry <- list(
    adam = list(read_path = "/study/data/adam",
                write_path = file.path(.sas2r_output_root, "datasets", "adam"),
                engine = "sas7bdat", write = "rds"),
    work = list(read_path = file.path(.sas2r_output_root, "work"),
                write_path = file.path(.sas2r_output_root, "work"),
                engine = "rds", write = "rds")
  )
})

A relative path means inside this folder; an absolute path is used as is. So a run folder can be moved or renamed as a whole – the session work library and every library the programs write travel with it – and only the source data the programs read, recorded absolutely, needs an edit when it moves. A commented <FILL> entry is a library some program reads and nothing binds: set its path and uncomment it. A LIBNAME inside a program still binds its libref where it stands, exactly as in SAS, so autoexec.R is the seed. Nothing under LOAD needs editing. A single program moved out of its folder stops with a message, because its runtime stayed behind.

Using the runtime interactively

Source autoexec.R once per session, with chdir = TRUE so its own folder is the working directory while it loads – the R counterpart of running a SAS autoexec. Load the library paths, helpers and formats into a dedicated environment. Programs sourced into that environment find the runtime already loaded and skip their own bootstrap:

runtime <- new.env()
source(file.path(bundle, "autoexec.R"), local = runtime, chdir = TRUE)

adsl <- runtime$lib_read("adam", "adsl") # honours the registry's paths and formats
adsl$USUBJID                         # column names resolve case-insensitively
runtime$sas_sort(adsl, by = "usubjid")

The helpers autoexec.R loads are the bundle’s own copy, which is the same code sas2r exports, so this works with or without sas2r installed; attach the package (library(sas2r)) when you want the help pages.

Two rules the data-access helpers enforce, because a wrong call in a translated program should fail loudly rather than be reinterpreted: lib_read() takes the libref and member as two separate strings, and lib_write() takes the data frame first – lib_write(df, "adam", "adsl"). Combined "lib.member" strings and single-argument calls are rejected with the canonical form in the message.

The reference

The full reference is the help topic:

library(sas2r)
?sas2r_runtime      # the overview, grouped
?lib_read           # each exported helper has its own page and examples

In brief, the runtime covers:

Group Helpers
Data access lib_read(), lib_write(), sas2r_libname_assign(), sas2r_libname_clear(), sas2r_fold_names(), the sas2r_dataset $/[[ methods
SAS value semantics chr_cmp(), %notin%, %+%, sas_if_else(), sas_sum(), sas_mean(), sas_min(), sas_max(), sas_round(), sas_length(), sas_substr(), sas_compress(), sas_display()
Sorting and merging sas_sort(), sas_merge()
Formats apply_format(), sas_put()
Includes sas2r_source_include()
Internal (generated code only, not exported) sas2r_registry_env(), sas2r_lib_entry(), sas2r_lib_member_path(), sas2r_libref_stop(), split_ds()

Every one of them reproduces a SAS behavior R does not share by default: missing values sort low and compare the SAS way, trailing blanks are cosmetic, rounding is half away from zero, SUM() and MEAN() are row-wise, MERGE follows SAS’s overlap and IN= rules.

Startup loads compiled PROC FORMAT definitions into .sas2r_formats. Generated programs and macro functions can use sas_put(x, "status.") or apply_format(x, "$status.") directly. Names ignore case and accept a trailing period; the $ prefix distinguishes character formats. An unknown name raises an error. The selected setup R file is source context, not an additional startup script, so its assignments do not create runtime bindings in smoke or bundle runs. Format widths and decimal specifications (for example, status8.) are unsupported and raise an explicit error; removing a width can change the source’s output. Conflicting definitions of the same format name are omitted from the catalog and reported by preflight. Execution is deferred because one startup catalog cannot preserve changes to a format during a SAS session. Equivalent repeated definitions remain supported, including reordered values and non-overlapping numeric ranges.

Outside a bundle, pass a compiled catalog explicitly:

catalog <- list(status = list(values = c("0" = "No", "1" = "Yes")))
sas_put(c(0, 1), "STATUS.", catalog = catalog)

For MERGE ... BY, values may persist within a BY group. They reset at the next group: if an input has no row for a key, its unique columns are missing for that key. Filling them from a preceding key changes the SAS behavior.

Versioning and stability

The first line of every vendored sas2r-helpers.R states which sas2r version generated it:

# sas2r runtime helpers -- vendored by sas2r 0.5.8. Do not edit by hand.

A bundle always loads its own copy of the runtime, so upgrading sas2r does not replace that copy. Results still depend on inputs, R and dependency versions; recheck the outputs when those change. Fixes to the runtime reach a bundle when it is translated again.

The promise behind that:

When finished with this example, remove its temporary copy:

unlink(bundle, recursive = TRUE)

Where this is going

The runtime is now an exported, versioned source of truth inside sas2r, vendored into bundles from that source. When usage justifies it, it becomes a separate sas2r.runtime package that organizations can validate once, with an opt-in mode that writes library(sas2r.runtime) into bundles.