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:
| 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:
- Existing helper contracts are stable, but correctness fixes may
change erroneous results. Exported bundles retain their own runtime
until regenerated.
- A helper is retired only after a replacement ships and the old name
has warned for at least one minor version.
- The lint allowlist, the help topic, and the runtime file are held in
sync by the package’s tests, so a helper cannot be added or removed in
one place only.
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.